This topic explains how to verify a digital signature.
Background
When you make an API call, such as CheckoutLicense or PushMeteringData, from an ECS instance in Compute Nest, the response from Compute Nest includes a digital signature in the Token field. As a service provider, you can calculate the digital signature and compare your calculated value with the Token value from the response. This process verifies that the response data has not been tampered with.
Digital signature verification process

Procedure
This example shows how to calculate a digital signature by using the response from a CheckoutLicense API call.
-
Call
CheckoutLicenseto get the response.curl -H "Content-Type: application/json" -XPOST https://cn-wulanchabu.axt.aliyun.com/computeNest/license/check_out_license -d '{}'Response
{ "code":200, "requestId":"4ea52d12-8e28-440b-b454-938d0518****", "instanceId":"i-0jl1ej1czubkimg6****", "result":{ "RequestId":"CF54B4C9-E54C-1405-9A37-A0FE3D60****", "ServiceInstanceId":"si-85a343279cf341c2****", "LicenseMetadata":"{\"TemplateName\":\"Custom_Image_Ecs\",\"SpecificationName\":\"dataDiskSize\",\"CustomData\":\"30T\"}", "Token":"21292abff855ab5c2a03809e0e4fb048", "ExpireTime":"2022-11-10T08:03:16Z" } } -
Extract all fields from the
resultobject except for theTokenfield. Sort the remaining fields alphabetically by key and concatenate them into a string by using ampersands (&).The concatenated string is as follows:
ExpireTime=2022-11-10T08:03:16Z&LicenseMetadata={"TemplateName":"Custom_Image_Ecs","SpecificationName":"dataDiskSize","CustomData":"30T"}&RequestId=CF54B4C9-E54C-1405-9A37-A0FE3D60****&ServiceInstanceId=si-85a343279cf341c2****NoteFor services that use
LicenseMetadata, note the following to prevent signature mismatches:-
Parameters in the template, such as the specification name and template name, must not contain Chinese characters.
-
When you build the signature string, you must convert the returned JSON string in the
LicenseMetadatafield to a compact format, for example,{"TemplateName":"Custom_Image_Ecs","SpecificationName":"dataDiskSize","CustomData":"30T"}.
-
-
Append your service key to the end of the sorted string.
Obtain your service key from the Service Key field on the Service Details page in the Compute Nest console.
After you append the service key, the string is as follows:
ExpireTime=2022-11-10T08:03:16Z&LicenseMetadata={"TemplateName":"Custom_Image_Ecs","SpecificationName":"dataDiskSize","CustomData":"30T"}&RequestId=CF54B4C9-E54C-1405-9A37-A0FE3D60****&ServiceInstanceId=si-85a343279cf341c2****&Key=37131c4a485141xxxxxx -
Compute an MD5 hash of the processed string. The output is a 32-character lowercase value.
After hashing, the resulting value is:
21292abff855ab5c2a03809e0e4fb048.Compare your calculated
Tokenvalue with the one returned by Compute Nest. If the two values are identical, the data has not been tampered with.
Sample code
The sample responses in the code are from Verify service instance validity and PushMeteringData - Push metering data.
Python
import json
import hashlib
def format_value(value):
"""Formats parameter values and handles different data types."""
if isinstance(value, bool):
return "true" if value else "false"
elif isinstance(value, dict):
# Handles dictionaries by creating a "key=value" string with pairs separated by a comma and a space.
items = []
for k, v in value.items():
# Converts boolean values to lowercase strings and other types to strings.
v_str = str(v).lower() if isinstance(v, bool) else str(v)
items.append(f"{k}={v_str}")
return "{" + ", ".join(items) + "}"
elif isinstance(value, list):
# Handles lists by converting them directly to the JSON format.
return json.dumps(value, separators=(',', ':'))
elif isinstance(value, str):
try:
# Attempts to parse the string as a JSON object.
parsed = json.loads(value)
if isinstance(parsed, dict):
# If the object is a dictionary, converts it to the standard JSON format.
return json.dumps(parsed, separators=(',', ':'))
elif isinstance(parsed, list):
return json.dumps(parsed, separators=(',', ':'))
else:
return value
except json.JSONDecodeError:
return value
else:
return str(value)
def calculate_token(response_json_str, service_key):
response = json.loads(response_json_str)
result = response.get("result", {})
# Extracts and formats all parameters except for Token.
params = {}
for key, value in result.items():
if key.lower() != "token":
formatted_value = format_value(value)
params[key] = formatted_value
# Sorts the parameters alphabetically by key.
sorted_params = sorted(params.items(), key=lambda x: x[0].lower())
# Generates the concatenated string.
query_string = "&".join(f"{k}={v}" for k, v in sorted_params)
print(f"Concatenated string to sign: {query_string}")
# Appends the service key.
final_str = f"{query_string}&Key={service_key}"
print(f"Final string with service key: {final_str}")
# Calculates the MD5 hash.
md5_hash = hashlib.md5(final_str.encode()).hexdigest().lower()
return md5_hash
# Example
if __name__ == "__main__":
# Sample response. Replace this with the actual response.
response_json = r'''{
"code": 200,
"requestId": "4ea52d12-8e28-440b-b454-938d0518****",
"instanceId": "i-0jl1ej1czubkimg6****",
"result": {
"RequestId": "CF54B4C9-E54C-1405-9A37-A0FE3D60****",
"ServiceInstanceId": "si-85a343279cf341c2****",
"LicenseMetadata": "{\"TemplateName\":\"Custom_Image_Ecs\",\"SpecificationName\":\"dataDiskSize\",\"CustomData\":\"30T\"}",
"Token": "21292abff855ab5c2a03809e0e4fb048",
"ExpireTime": "2022-11-10T08:03:16Z"
}
}'''
service_key = "37131c4a485141xxxxxx" # Replace this with your actual service key.
calculated_token = calculate_token(response_json, service_key)
print(f"Calculated Token: {calculated_token}")
print(f"Returned Token: {json.loads(response_json)['result']['Token']}")
# Verifies whether the tokens match.
print(f"Verification result: {calculated_token == json.loads(response_json)['result']['Token']}")
Java
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.security.MessageDigest;
import java.util.*;
public class TokenVerifier {
private static final String REQUEST_ID = "RequestId";
private static final String TOKEN_KEY = "Token";
// --- Test case ---
public static void main(String[] args) {
String responseJson = "{\n" +
" \"code\": 200,\n" +
" \"result\": {\n" +
" \"ExpireTime\": \"2022-11-02T02:39:43Z\",\n" +
" \"LicenseMetadata\": \"{\\\"TemplateName\\\":\\\"Custom_Image_Ecs\\\",\\\"SpecificationName\\\":\\\"dataDiskSize\\\",\\\"CustomData\\\":\\\"30T\\\"}\",\n" +
" \"RequestId\": \"CF54B4C9-E54C-1405-9A37-A0FE3D60****\",\n" +
" \"ServiceInstanceId\": \"si-85a343279cf341c2****\",\n" +
" \"Token\": \"21292abff855ab5c2a03809e0e4fb048\"\n" +
" }\n" +
"}";
String serviceProviderKey = "37131c4a485141xxxxxx";
boolean isValid = verifyToken(responseJson, serviceProviderKey);
System.out.println("Token verification result: " + isValid);
}
/**
* Calculates and verifies the token from a JSON response.
*/
public static boolean verifyToken(String responseJsonStr, String serviceProviderKey) {
try {
// 1. Parse the JSON response.
ObjectMapper objectMapper = new ObjectMapper();
JsonNode responseNode = objectMapper.readTree(responseJsonStr);
JsonNode resultNode = responseNode.get("result");
// 2. Extract and format the parameters.
Map<String, String> params = new HashMap<>();
Iterator<String> fieldNames = resultNode.fieldNames();
while (fieldNames.hasNext()) {
String key = fieldNames.next();
if (!key.equalsIgnoreCase(TOKEN_KEY)) {
Object value = resultNode.get(key);
String formattedValue = formatValue(value);
params.put(key, formattedValue);
}
}
// 3. Sort the parameters.
List<Map.Entry<String, String>> sortedParams = new ArrayList<>(params.entrySet());
sortedParams.sort(Comparator.comparing(entry -> entry.getKey().toLowerCase()));
// 4. Generate the concatenated string.
String urlParams = buildOrderUrlParams(sortedParams);
String finalStr = urlParams + "&Key=" + serviceProviderKey;
System.out.println("Final string to sign: " + finalStr);
// 5. Calculate the MD5 hash.
String calculatedToken = generateMD5(finalStr);
// 6. Get the returned token.
String returnedToken = resultNode.get(TOKEN_KEY).asText();
System.out.println("Calculated Token: " + calculatedToken);
System.out.println("Returned Token: " + returnedToken);
// 7. Verify whether the tokens match.
return calculatedToken.equals(returnedToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
private static String formatValue(Object value) {
if (value instanceof Boolean) {
return (Boolean) value ? "true" : "false";
} else if (value instanceof JsonNode) {
JsonNode node = (JsonNode) value;
if (node.isObject()) {
// Handles object types by creating a "key=value" string.
List<String> items = new ArrayList<>();
Iterator<String> fieldNames = node.fieldNames();
while (fieldNames.hasNext()) {
String key = fieldNames.next();
Object childValue = node.get(key);
String formattedChildValue = formatValue(childValue);
items.add(key + "=" + formattedChildValue);
}
return "{" + String.join(", ", items) + "}";
} else if (node.isArray()) {
// Handles array types as needed.
return node.toString().replace(" ", "");
} else {
return node.asText();
}
} else {
return value.toString();
}
}
private static String buildOrderUrlParams(List<Map.Entry<String, String>> entries) {
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, String> entry : entries) {
sb.append(entry.getKey())
.append("=")
.append(entry.getValue())
.append("&");
}
if (sb.length() > 0) {
sb.setLength(sb.length() - 1); // Remove the trailing ampersand (&).
}
return sb.toString();
}
private static String generateMD5(String input) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] hash = md.digest(input.getBytes());
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = String.format("%02x", b);
hexString.append(hex);
}
return hexString.toString();
} catch (Exception e) {
throw new RuntimeException("Failed to calculate MD5 hash", e);
}
}
}
Ensure that your project includes the com.fasterxml.jackson.core library, for example, as a Maven dependency:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version> <!-- We recommend that you use the latest stable version. -->
</dependency>