All Products
Search
Document Center

Alibaba Cloud SDK:v3 request body and signature mechanism

Last Updated:Oct 11, 2025

If you prefer not to use an SDK to call Alibaba Cloud OpenAPI, or if your application's runtime environment does not support SDKs, you can call Alibaba Cloud OpenAPI by self-signing your requests. This topic describes the v3 signature mechanism to help you call Alibaba Cloud OpenAPI directly using HTTP requests.

Usage notes

  • You can directly replace v2 signatures with v3 signatures for API calls.

  • The OpenAPI portal provides SDKs for Alibaba Cloud products. The APIs for these products support v3 signatures. Note that some cloud products use self-managed gateways and have authentication mechanisms that differ from the one described in this topic. When you send HTTP requests to these products, refer to their respective signature mechanism documents.

HTTP request structure

A complete Alibaba Cloud OpenAPI request consists of the following parts.

Name

Required

Description

Example

Protocol

Yes

You can configure this by referring to the API reference for different cloud products. Requests can be sent over HTTP or HTTPS. For better security, use HTTPS. Valid values are https:// or http://.

https://

Endpoint

Yes

The endpoint. You can find the endpoints for different areas where the endpoint is deployed in the endpoint documentation for each cloud product.

ecs.cn-shanghai.aliyuncs.com

resource_URI_parameters

Yes

The API URL, which includes the API path and request parameters located in the path and query.

ImageId=win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd&RegionId=cn-shanghai

RequestHeader

Yes

Common request headers, which usually include the API version, Host, and Authorization information. This is described in detail later.

Authorization: ACS3-HMAC-SHA256 Credential=YourAccessKeyId,SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version,Signature=06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0

x-acs-action: RunInstances

host: ecs.cn-shanghai.aliyuncs.com

x-acs-date: 2023-10-26T09:01:01Z

x-acs-version: 2014-05-26

x-acs-content-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

x-acs-signature-nonce: d410180a5abf7fe235dd9b74aca91fc0

RequestBody

Yes

Business request parameters defined in the body. You can obtain them from the API metadata.

HTTPMethod

Yes

The request method. You can obtain it from the API metadata.

POST

RequestHeader

When you call an Alibaba Cloud OpenAPI, the common request headers must include the following information.

Name

Type

Required

Description

Example

host

String

Yes

The endpoint. For more information, see HTTP request structure.

ecs.cn-shanghai.aliyuncs.com

x-acs-action

String

Yes

The name of the API. You can visit the Alibaba Cloud OpenAPI Developer Portal to search for the OpenAPI you want to call.

RunInstances

x-acs-content-sha256

String

Yes

The result of hashing the RequestBody and then Base16 encoding it. This value is the same as HashedRequestPayload.

e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

x-acs-date

String

Yes

The UTC time in ISO 8601 format: yyyy-MM-ddTHH:mm:ssZ. For example, 2018-01-01T12:00:00Z. The value must be a time within 15 minutes before the request is sent.

2023-10-26T10:22:32Z

x-acs-signature-nonce

String

Yes

A unique random number for the signature. This number prevents replay attacks. Use a different random number for each request. This mechanism applies only to the HTTP protocol.

3156853299f313e23d1673dc12e1703d

x-acs-version

String

Yes

The API version. For information about how to obtain the version, see How do I get the API version (x-acs-version)?.

2014-05-26

Authorization

String

Required for non-anonymous requests

The authentication information used to verify the request's legitimacy. The format is Authorization: SignatureAlgorithm Credential=AccessKeyId,SignedHeaders=SignedHeaders,Signature=Signature.

SignatureAlgorithm is the signature encryption method, which is ACS3-HMAC-SHA256.

Credential is the user's AccessKey ID. You can view your AccessKey ID in the RAM console. To create an AccessKey pair, see Create an AccessKey pair.

SignedHeaders specifies the keys of the request headers that are included in the signature. Note: For better security, sign all common request headers except for Authorization.

Signature is the request signature. For its value, see Signature mechanism.

ACS3-HMAC-SHA256 Credential=YourAccessKeyId,SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version,Signature=06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0

x-acs-security-token

String

Required for STS authentication

The value of SecurityToken in the response returned by calling the AssumeRole operation.

Signature mechanism

Signatures are authenticated using an AccessKey pair. For each HTTP or HTTPS request, Alibaba Cloud API Gateway recalculates the signature based on the request parameters. The gateway then compares this signature with the one provided in the request to verify the requester's identity. This process ensures data integrity and security.

Important

Requests and responses are encoded in the UTF-8 character set.

Step 1: Construct a canonical request

The following pseudocode shows how to construct a canonical request (CanonicalRequest):

CanonicalRequest =
  HTTPRequestMethod + '\n' +    // HTTP request method, in uppercase
  CanonicalURI + '\n' +         // Canonical URI
  CanonicalQueryString + '\n' + // Canonical query string
  CanonicalHeaders + '\n' +     // Canonical headers
  SignedHeaders + '\n' +        // Signed headers
  HashedRequestPayload		// The value of the RequestBody after it is hashed

HTTPRequestMethod (request method)

The uppercase HTTP method name, such as GET or POST.

CanonicalURI (canonical URI)

The canonical URI is the encoded resource path of the URL. The resource path is the part of the URL between the host and the query string. It includes the / after the host but not the ? before the query string. You must encode each part of the URI (the strings separated by /) using the UTF-8 character set according to the rules in RFC3986:

  • Characters A-Z, a-z, 0-9, and the characters -, _, ., and ~ are not encoded.

    Other characters are encoded as % followed by the character's ASCII code in hexadecimal format. For example, a half-width double quotation mark (") is encoded as %22. Note that some special characters require special handling.

    Before encoding

    After encoding

    Space ( )

    %20

    Asterisk (*)

    %2A

    %7E

    Tilde (~)

If you use java.net.URLEncoder from the Java standard library, you can first encode the string using the encode method. Then, you must replace the plus sign (+) with %20, the asterisk (*) with %2A, and %7E with a tilde (~) to obtain the encoded string that complies with the preceding rules.

Important

For RPC-style APIs, use a forward slash (/) as the canonical URI.

For ROA-style APIs, this parameter is the value of the path parameter in the OpenAPI metadata, such as /api/v1/clusters.

CanonicalQueryString (canonical query string)

In the API metadata, if a request parameter is specified to be in the query ("in":"query"), you must concatenate all such parameters as follows:

  1. Sort the request parameters by name in ascending alphabetical order.

  2. URI-encode each parameter name and value using the UTF-8 character set according to the rules in RFC3986. The encoding rules are the same as the rules for encoding the canonical URI.

  3. Connect the encoded parameter name and value with an equal sign (=). If a parameter has no value, use an empty string for its value.

  4. Connect multiple request parameters with ampersands (&).

Important
  • If a request parameter is of the array or object type, you must convert the parameter value into indexed key-value pairs. For more information, see How do I pass parameters of the array or object type?.

  • If a request parameter is a JSON string, the order of parameters in the JSON string does not affect the signature calculation.

  • If no query string exists, use an empty string as the canonical query string.

Example:

ImageId=win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd&RegionId=cn-shanghai

HashedRequestPayload

Hash the request body and then Base16-encode the hash value to obtain the hashed request payload. Update the value of the x-acs-content-sha256 header in the request header to the value of the hashed request payload. The following pseudocode shows how to calculate the hashed request payload:

HashedRequestPayload = HexEncode(Hash(RequestBody))
  • In the API metadata, if a request parameter for the API is set to "in": "body" or "in": "formData", you must pass the parameters in the RequestBody:

    Note

    If no request parameters are passed in the request body, the request body is an empty string.

    • If the request parameters include "in": "formData", you must concatenate the parameters into a string in the format key1=value1&key2=value2&key3=value3. You must also add content-type=application/x-www-form-urlencoded to the request header. Note that if a request parameter is an array or object, you must convert the parameter value to indexed key-value pairs.

    • If the request parameters include "in": "body", you must add the `Content-Type` header to the request. The value of this header depends on the content type of the request. For example:

      • If the request content is JSON data, the content-type is application/json.

      • If the request content is a binary file stream, the content-type is application/octet-stream.

  • Hash represents the message digest function. Only the SHA256 algorithm is supported.

  • HexEncode represents the encoding function that returns the digest in lowercase hexadecimal format (Base16 encoding).

The following example shows the value of the hashed request payload when the request body is empty:

e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

CanonicalHeaders (canonical request headers)

Concatenate the parameters in the request header as follows:

  1. Filter the request header to select parameters that have the x-acs- prefix, or are named host or content-type.

  2. Convert the parameter names to lowercase and sort them in alphabetical order.

  3. Trim leading and trailing spaces from the parameter values.

  4. Connect the parameter name and value with a colon (:) and add a line feed (\n) at the end to form a canonical header entry.

  5. Concatenate multiple canonical header entries into a single string.

Note

All request headers except for the Authorization header must be included in the signature if they meet the requirements.

The pseudocode is as follows:

CanonicalHeaderEntry = Lowercase(HeaderName) + ':' + Trim(HeaderValue) + '\n'

CanonicalHeaders = 
    CanonicalHeaderEntry0 + CanonicalHeaderEntry1 + ... + CanonicalHeaderEntryN

Example:

host:ecs.cn-shanghai.aliyuncs.com
x-acs-action:RunInstances
x-acs-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-acs-date:2023-10-26T10:22:32Z
x-acs-signature-nonce:3156853299f313e23d1673dc12e1703d
x-acs-version:2014-05-26

SignedHeaders (list of signed headers)

This parameter specifies the common request headers that are included in the signature for the request. It corresponds to the parameter names in the canonical headers. Construct the list of signed headers as follows:

  • Convert the names of the headers that are included in the canonical headers to lowercase.

  • Sort the header names in alphabetical order and separate them with semicolons (;).

The pseudocode is as follows:

SignedHeaders = Lowercase(HeaderName0) + ';' + Lowercase(HeaderName1) + ... + Lowercase(HeaderNameN) 

Example:

host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version

Step 2: Construct the string to sign

Construct the string to sign (stringToSign) based on the following pseudocode:

StringToSign =
    SignatureAlgorithm + '\n' +
    HashedCanonicalRequest
  • SignatureAlgorithm

    The signature protocol supports only the ACS3-HMAC-SHA256 algorithm.

  • HashedCanonicalRequest

    The hashed canonical request string. The following pseudocode shows how to calculate the hashed canonical request:

    HashedCanonicalRequest = HexEncode(Hash(CanonicalRequest))
    • Hash represents the message digest function. Only the SHA256 algorithm is supported.

    • HexEncode represents the encoding function that returns the digest in lowercase hexadecimal format (Base16 encoding).

Example:

ACS3-HMAC-SHA256
7ea06492da5221eba5297e897ce16e55f964061054b7695beedaac1145b1e259

Step 3: Calculate the signature

Calculate the signature value (Signature) based on the following pseudocode.

Signature = HexEncode(SignatureMethod(Secret, StringToSign))
  • StringToSign: The string to sign that is constructed in Step 2 and encoded in UTF-8.

  • SignatureMethod: Use HMAC-SHA256 as the signature algorithm.

  • Secret: The AccessKey secret.

  • HexEncode: The encoding function that returns the digest in lowercase hexadecimal format (Base16 encoding).

Example:

06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0

Step 4: Add the signature to the request

After you calculate the signature, construct the Authorization request header in the following format: Authorization: <b>SignatureAlgorithm</b> Credential=<b>AccessKeyId</b>,SignedHeaders=<b>SignedHeaders</b>,Signature=<b>Signature</b>.

Example:

ACS3-HMAC-SHA256 Credential=YourAccessKeyId,SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version,Signature=06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0

Signature example code

To help you better understand the signature mechanism, this topic provides complete implementations in major programming languages. The example code is provided to help you understand the signature mechanism and may not be universally applicable. Alibaba Cloud OpenAPI provides SDKs for various programming languages and development frameworks. If you use these SDKs, you do not need to perform the signature process. This lets you quickly build applications that are related to Alibaba Cloud. We recommend that you use the SDKs.

Important

Before you sign a request, make sure to check the API metadata to obtain information such as the API's request method, request parameter names, request parameter types, and how parameters are passed. Otherwise, the signature is likely to fail.

Fixed parameter example

This example uses assumed parameter values to demonstrate the correct output at each step of the signature mechanism. You can use the assumed parameter values from this example in your code for calculation and compare your output with the content of this example to verify that your signature process is correct.

Required parameter name

Assumed parameter value

AccessKeyID

YourAccessKeyId

AccessKeySecret

YourAccessKeySecret

x-acs-signature-nonce

3156853299f313e23d1673dc12e1703d

x-acs-date

2023-10-26T10:22:32Z

x-acs-action

RunInstances

x-acs-version

2014-05-26

host

ecs.cn-shanghai.aliyuncs.com

API request parameters:

ImageId

win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd

RegionId

cn-shanghai

The signature flow is as follows:

  1. Construct a canonical request.

POST
/
ImageId=win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd&RegionId=cn-shanghai
host:ecs.cn-shanghai.aliyuncs.com
x-acs-action:RunInstances
x-acs-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-acs-date:2023-10-26T10:22:32Z
x-acs-signature-nonce:3156853299f313e23d1673dc12e1703d
x-acs-version:2014-05-26

host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
  1. Construct the string to sign.

ACS3-HMAC-SHA256
7ea06492da5221eba5297e897ce16e55f964061054b7695beedaac1145b1e259
  1. Calculate the signature.

06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0
  1. Add the signature to the request.

POST /?ImageId=win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd&RegionId=cn-shanghai HTTP/1.1
Authorization: ACS3-HMAC-SHA256 Credential=YourAccessKeyId,SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version,Signature=06563a9e1b43f5dfe96b81484da74bceab24a1d853912eee15083a6f0f3283c0
x-acs-action: RunInstances
host: ecs.cn-shanghai.aliyuncs.com
x-acs-date: 2023-10-26T09:01:01Z
x-acs-version: 2014-05-26
x-acs-content-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-acs-signature-nonce: d410180a5abf7fe235dd9b74aca91fc0
user-agent: AlibabaCloud (Mac OS X; x86_64) Java/1.8.0_352-b08 tea-util/0.2.6 TeaDSL/1
accept: application/json

Java example

Note

The example code runs in JDK 1.8. You may need to adjust the code based on your specific situation.

To run the Java example, add the following Maven dependencies to your pom.xml file.

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.13</version>
</dependency>
<dependency>
     <groupId>com.google.code.gson</groupId>
     <artifactId>gson</artifactId>
     <version>2.9.0</version>
 </dependency>
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import org.apache.http.client.methods.*;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;

public class SignatureDemo {

    public static class SignatureRequest {
        // HTTP Method
        private final String httpMethod;
        // Request path
        private final String canonicalUri;
        // Endpoint
        private final String host;
        // API name
        private final String xAcsAction;
        // API version
        private final String xAcsVersion;
        // Headers
        private final Map<String, String> headers = new TreeMap<>();
        // Body parameters
        private byte[] body;
        // Query parameters
        private final Map<String, Object> queryParam = new TreeMap<>();

        public SignatureRequest(String httpMethod, String canonicalUri, String host,
                                String xAcsAction, String xAcsVersion) {
            this.httpMethod = httpMethod;
            this.canonicalUri = canonicalUri;
            this.host = host;
            this.xAcsAction = xAcsAction;
            this.xAcsVersion = xAcsVersion;
            initHeader();
        }

        private void initHeader() {
            headers.put("host", host);
            headers.put("x-acs-action", xAcsAction);
            headers.put("x-acs-version", xAcsVersion);

            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
            sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
            headers.put("x-acs-date", sdf.format(new Date()));
            headers.put("x-acs-signature-nonce", UUID.randomUUID().toString());
        }

        public String getHttpMethod() {
            return httpMethod;
        }

        public String getCanonicalUri() {
            return canonicalUri;
        }

        public String getHost() {
            return host;
        }

        public Map<String, String> getHeaders() {
            return headers;
        }

        public byte[] getBody() {
            return body;
        }

        public Map<String, Object> getQueryParam() {
            return queryParam;
        }

        public void setBody(byte[] body) {
            this.body = body;
        }

        public void setQueryParam(String key, Object value) {
            this.queryParam.put(key, value);
        }

        public void setHeaders(String key, String value) {
            this.headers.put(key, value);
        }
    }

    public static class SignatureService {
        private static final String ALGORITHM = "ACS3-HMAC-SHA256";

        /**
         * Calculate the signature
         */
        public static void getAuthorization(SignatureRequest signatureRequest,
                                            String accessKeyId, String accessKeySecret, String securityToken) {
            try {
                // Process complex query parameters
                Map<String, Object> processedQueryParams = new TreeMap<>();
                processObject(processedQueryParams, "", signatureRequest.getQueryParam());
                signatureRequest.getQueryParam().clear();
                signatureRequest.getQueryParam().putAll(processedQueryParams);

                // Step 1: Construct the canonical request string
                String canonicalQueryString = buildCanonicalQueryString(signatureRequest.getQueryParam());

                // Calculate the request body hash
                String hashedRequestPayload = calculatePayloadHash(signatureRequest.getBody());
                signatureRequest.setHeaders("x-acs-content-sha256", hashedRequestPayload);

                // Add security token if it exists
                if (securityToken != null && !securityToken.isEmpty()) {
                    signatureRequest.setHeaders("x-acs-security-token", securityToken);
                }

                // Build canonical headers and signed headers
                CanonicalHeadersResult canonicalHeadersResult = buildCanonicalHeaders(signatureRequest.getHeaders());

                // Build the canonical request
                String canonicalRequest = String.join("\n",
                        signatureRequest.getHttpMethod(),
                        signatureRequest.getCanonicalUri(),
                        canonicalQueryString,
                        canonicalHeadersResult.canonicalHeaders,
                        canonicalHeadersResult.signedHeaders,
                        hashedRequestPayload);

                System.out.println("canonicalRequest=========>\n" + canonicalRequest);

                // Step 2: Construct the string to sign
                String hashedCanonicalRequest = sha256Hex(canonicalRequest.getBytes(StandardCharsets.UTF_8));
                String stringToSign = ALGORITHM + "\n" + hashedCanonicalRequest;
                System.out.println("stringToSign=========>\n" + stringToSign);

                // Step 3: Calculate the signature
                String signature = DatatypeConverter.printHexBinary(
                                hmac256(accessKeySecret.getBytes(StandardCharsets.UTF_8), stringToSign))
                        .toLowerCase();
                System.out.println("signature=========>" + signature);

                // Step 4: Build the Authorization header
                String authorization = String.format("%s Credential=%s,SignedHeaders=%s,Signature=%s",
                        ALGORITHM, accessKeyId, canonicalHeadersResult.signedHeaders, signature);

                System.out.println("authorization=========>" + authorization);
                signatureRequest.getHeaders().put("Authorization", authorization);
            } catch (Exception e) {
                throw new RuntimeException("Failed to generate authorization", e);
            }
        }

        /**
         * Handle parameters of the formData request parameter type.
         */
        private static String formDataToString(Map<String, Object> formData) {
            Map<String, Object> tileMap = new HashMap<>();
            processObject(tileMap, "", formData);
            StringBuilder result = new StringBuilder();
            boolean first = true;
            String symbol = "&";
            for (Map.Entry<String, Object> entry : tileMap.entrySet()) {
                String value = String.valueOf(entry.getValue());
                if (value != null && !value.isEmpty()) {
                    if (first) {
                        first = false;
                    } else {
                        result.append(symbol);
                    }
                    result.append(percentCode(entry.getKey()));
                    result.append("=");
                    result.append(percentCode(value));
                }
            }

            return result.toString();
        }

        /**
         * Build the canonical query string
         */
        private static String buildCanonicalQueryString(Map<String, Object> queryParams) {
            return queryParams.entrySet().stream()
                    .map(entry -> percentCode(entry.getKey()) + "=" +
                            percentCode(String.valueOf(entry.getValue())))
                    .collect(Collectors.joining("&"));
        }

        /**
         * Calculate the request body hash value
         */
        private static String calculatePayloadHash(byte[] body) throws Exception {
            if (body != null) {
                return sha256Hex(body);
            } else {
                return sha256Hex("".getBytes(StandardCharsets.UTF_8));
            }
        }

        /**
         * Build the canonical header information
         */
        private static CanonicalHeadersResult buildCanonicalHeaders(Map<String, String> headers) {
            List<Map.Entry<String, String>> signedHeaders = headers.entrySet().stream()
                    .filter(entry -> {
                        String key = entry.getKey().toLowerCase();
                        return key.startsWith("x-acs-") || "host".equals(key) || "content-type".equals(key);
                    })
                    .sorted(Map.Entry.comparingByKey())
                    .collect(Collectors.toList());

            StringBuilder canonicalHeaders = new StringBuilder();
            StringBuilder signedHeadersString = new StringBuilder();

            for (Map.Entry<String, String> entry : signedHeaders) {
                String lowerKey = entry.getKey().toLowerCase();
                String value = entry.getValue().trim();
                canonicalHeaders.append(lowerKey).append(":").append(value).append("\n");
                signedHeadersString.append(lowerKey).append(";");
            }

            if (signedHeadersString.length() > 0) {
                signedHeadersString.setLength(signedHeadersString.length() - 1); // Remove the last semicolon
            }

            return new CanonicalHeadersResult(canonicalHeaders.toString(), signedHeadersString.toString());
        }

        private static class CanonicalHeadersResult {
            final String canonicalHeaders;
            final String signedHeaders;

            CanonicalHeadersResult(String canonicalHeaders, String signedHeaders) {
                this.canonicalHeaders = canonicalHeaders;
                this.signedHeaders = signedHeaders;
            }
        }

        /**
         * Process complex object parameters
         */
        private static void processObject(Map<String, Object> map, String key, Object value) {
            if (value == null) {
                return;
            }

            if (key == null) {
                key = "";
            }

            if (value instanceof List<?>) {
                List<?> list = (List<?>) value;
                for (int i = 0; i < list.size(); ++i) {
                    processObject(map, key + "." + (i + 1), list.get(i));
                }
            } else if (value instanceof Map<?, ?>) {
                Map<?, ?> subMap = (Map<?, ?>) value;
                for (Map.Entry<?, ?> entry : subMap.entrySet()) {
                    processObject(map, key + "." + entry.getKey().toString(), entry.getValue());
                }
            } else {
                if (key.startsWith(".")) {
                    key = key.substring(1);
                }

                if (value instanceof byte[]) {
                    map.put(key, new String((byte[]) value, StandardCharsets.UTF_8));
                } else {
                    map.put(key, String.valueOf(value));
                }
            }
        }

        /**
         * HMAC-SHA256 calculation
         */
        private static byte[] hmac256(byte[] secretKey, String str) throws Exception {
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, mac.getAlgorithm());
            mac.init(secretKeySpec);
            return mac.doFinal(str.getBytes(StandardCharsets.UTF_8));
        }

        /**
         * SHA-256 hash calculation
         */
        private static String sha256Hex(byte[] input) throws Exception {
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            byte[] digest = md.digest(input);
            return DatatypeConverter.printHexBinary(digest).toLowerCase();
        }

        /**
         * URL encoding
         */
        public static String percentCode(String str) {
            if (str == null) {
                return "";
            }
            try {
                return URLEncoder.encode(str, "UTF-8")
                        .replace("+", "%20")
                        .replace("*", "%2A")
                        .replace("%7E", "~");
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException("UTF-8 encoding not supported", e);
            }
        }
    }

    /**
     * Signature example. Replace the example parameters in the main method as needed.
     * The logic for getting the canonicalUri value is the only difference between ROA and RPC APIs. The rest is similar.
     * <p>
     * Get the request method (methods), request parameter name (name), request parameter type (type), and request parameter location (in) from the API metadata, and encapsulate the parameters into SignatureRequest.
     * 1. If the request parameter is shown as "in":"query" in the metadata, pass the parameter using queryParam. Note: For RPC APIs, this type of parameter can also be passed through the body with content-type as application/x-www-form-urlencoded. See Example 3.
     * 2. If the request parameter is shown as "in": "body" in the metadata, pass the parameter through the body. The MIME type is application/octet-stream or application/json. Note: For RPC APIs, it is not recommended to use application/json. You can use Example 3 instead.
     * 3. If the request parameter is shown as "in": "formData" in the metadata, pass the parameter through the body. The MIME type is application/x-www-form-urlencoded.
     */
    public static void main(String[] args) throws IOException {
        // Get AccessKey from environment variables
        String accessKeyId = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID");
        String accessKeySecret = System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET");
        String securityToken = System.getenv("ALIBABA_CLOUD_SECURITY_TOKEN");

        if (accessKeyId == null || accessKeySecret == null) {
            System.err.println("Set the ALIBABA_CLOUD_ACCESS_KEY_ID and ALIBABA_CLOUD_ACCESS_KEY_SECRET environment variables.");
            return;
        }

        // RPC API example 1: Request parameters are in "query". This example uses DescribeInstanceStatus of ECS.
        SignatureRequest signatureRequest = new SignatureRequest(
                "POST",
                "/",
                "ecs.cn-hangzhou.aliyuncs.com",
                "DescribeInstanceStatus",
                "2014-05-26"
        );
        signatureRequest.setQueryParam("RegionId", "cn-hangzhou");
        signatureRequest.setQueryParam("InstanceId", Arrays.asList("i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"));

        /*// RPC API example 2: Request parameters are in "body" (file upload scenario). This example uses RecognizeGeneral of OCR.
        SignatureRequest signatureRequest = new SignatureRequest(
                "POST",
                "/",
                "ocr-api.cn-hangzhou.aliyuncs.com",
                "RecognizeGeneral",
                "2021-07-07");
        signatureRequest.setBody(Files.readAllBytes(Paths.get("D:\\test.jpeg")));
        signatureRequest.setHeaders("content-type", "application/octet-stream");*/

        /*// RPC API example 3: Request parameters are in "formData" or "in":"body" (non-file upload scenario). This example uses TranslateGeneral of Machine Translation.
        String httpMethod = "POST";
        String canonicalUri = "/";
        String host = "mt.aliyuncs.com";
        String xAcsAction = "TranslateGeneral";
        String xAcsVersion = "2018-10-12";
        SignatureRequest signatureRequest = new SignatureRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
        Map<String, Object> body = new HashMap<>();
        body.put("FormatType", "text");
        body.put("SourceLanguage", "zh");
        body.put("TargetLanguage", "en");
        body.put("SourceText", "Hello");
        body.put("Scene", "general");
        String formDataToString = SignatureService.formDataToString(body);
        signatureRequest.setBody(formDataToString.getBytes(StandardCharsets.UTF_8));
        signatureRequest.setHeaders("content-type", "application/x-www-form-urlencoded");*/

        /*// ROA API POST request example. This example uses creating a cluster in Container Service.
        SignatureRequest signatureRequest = new SignatureRequest(
                "POST",
                "/clusters",
                "cs.cn-chengdu.aliyuncs.com",
                "CreateCluster",
                "2015-12-15");
        TreeMap<String, Object> body = new TreeMap<>();
        body.put("name", "test");
        body.put("cluster_type", "ManagedKubernetes");
        body.put("kubernetes_version", "1.34.1-aliyun.1");
        body.put("region_id", "cn-chengdu");
        body.put("snat_entry", true);
        body.put("deletion_protection", true);
        body.put("proxy_mode", "ipvs");
        body.put("profile", "Default");
        body.put("timezone", "Asia/Shanghai");
        body.put("cluster_spec", "ack.pro.small");
        body.put("enable_rrsa", false);
        body.put("service_cidr", "192.168.0.0/16");
        body.put("zone_ids", Arrays.asList("cn-chengdu-b","cn-chengdu-b"));
        Gson gson = (new GsonBuilder()).disableHtmlEscaping().create();
        signatureRequest.setBody(gson.toJson(body).getBytes(StandardCharsets.UTF_8));
        signatureRequest.setHeaders("content-type", "application/json");*/

        /*// ROA API GET request. This example uses querying cluster information in Container Service.
        SignatureRequest signatureRequest = new SignatureRequest(
                "GET",
                "/clusters/" + SignatureService.percentCode("c299f90b63b************") + "/resources",
                "cs.cn-chengdu.aliyuncs.com",
                "DescribeClusterResources",
                "2015-12-15");
        signatureRequest.setQueryParam("with_addon_resources", true);*/

        /*// ROA API DELETE request. This example uses deleting a cluster.
        SignatureRequest signatureRequest = new SignatureRequest(
                "DELETE",
                "/clusters/" + SignatureService.percentCode("c299f90b63b************"),
                "cs.cn-chengdu.aliyuncs.com",
                "DeleteCluster",
                "2015-12-15");*/

        // Generate the signature
        SignatureService.getAuthorization(signatureRequest, accessKeyId, accessKeySecret, securityToken);

        // Test if the API can be called successfully
        callApi(signatureRequest);
    }

    /**
     * For testing only
     */
    private static void callApi(SignatureRequest signatureRequest) {
        try {
            String url = "https://" + signatureRequest.getHost() + signatureRequest.getCanonicalUri();
            URIBuilder uriBuilder = new URIBuilder(url);

            // Add query parameters
            for (Map.Entry<String, Object> entry : signatureRequest.getQueryParam().entrySet()) {
                uriBuilder.addParameter(entry.getKey(), String.valueOf(entry.getValue()));
            }
            HttpUriRequest httpRequest;
            switch (signatureRequest.getHttpMethod()) {
                case "GET":
                    httpRequest = new HttpGet(uriBuilder.build());
                    break;
                case "POST":
                    HttpPost httpPost = new HttpPost(uriBuilder.build());
                    if (signatureRequest.getBody() != null) {
                        httpPost.setEntity(new ByteArrayEntity(signatureRequest.getBody(), ContentType.create(signatureRequest.getHeaders().get("content-type"))));
                    }
                    httpRequest = httpPost;
                    break;
                case "DELETE":
                    httpRequest = new HttpDelete(uriBuilder.build());
                    break;
                default:
                    System.out.println("Unsupported HTTP method: " + signatureRequest.getHttpMethod());
                    throw new IllegalArgumentException("Unsupported HTTP method");
            }

            // Add request headers
            for (Map.Entry<String, String> entry : signatureRequest.getHeaders().entrySet()) {
                httpRequest.addHeader(entry.getKey(), entry.getValue());
            }

            // Send the request
            try (CloseableHttpClient httpClient = HttpClients.createDefault();
                 CloseableHttpResponse response = httpClient.execute(httpRequest)) {
                String result = EntityUtils.toString(response.getEntity(), "UTF-8");
                System.out.println("API Response: " + result);
            }
        } catch (IOException | URISyntaxException e) {
            throw new RuntimeException("Failed to call API", e);
        }
    }
}

Python example

Note

The example code runs in Python 3.12.3. You may need to adjust the code based on your specific situation.

You must manually install pytz and requests. Run the following command in the terminal based on your Python version.

Python 3

pip3 install pytz
pip3 install requests
import hashlib
import hmac
import json
import os
import uuid
from collections import OrderedDict
from datetime import datetime
from typing import Any, Dict, List, Optional, Union
from urllib.parse import quote_plus, urlencode

import pytz
import requests


class SignatureRequest:
    def __init__(
            self,
            http_method: str,
            canonical_uri: str,
            host: str,
            x_acs_action: str,
            x_acs_version: str
    ):
        self.http_method = http_method
        self.canonical_uri = canonical_uri
        self.host = host
        self.x_acs_action = x_acs_action
        self.x_acs_version = x_acs_version
        self.headers = self._init_headers()
        self.query_param = OrderedDict()  # type: Dict[str, Any]
        self.body = None  # type: Optional[bytes]

    def _init_headers(self) -> Dict[str, str]:
        current_time = datetime.now(pytz.timezone('Etc/GMT'))
        headers = OrderedDict([
            ('host', self.host),
            ('x-acs-action', self.x_acs_action),
            ('x-acs-version', self.x_acs_version),
            ('x-acs-date', current_time.strftime('%Y-%m-%dT%H:%M:%SZ')),
            ('x-acs-signature-nonce', str(uuid.uuid4())),
        ])
        return headers

    def sorted_query_params(self) -> None:
        """Sorts query parameters by name and returns the encoded string."""
        self.query_param = dict(sorted(self.query_param.items()))

    def sorted_headers(self) -> None:
        """Sorts request headers by name and returns the encoded string."""
        self.headers = dict(sorted(self.headers.items()))


def get_authorization(request: SignatureRequest) -> None:
    try:
        new_query_param = OrderedDict()
        process_object(new_query_param, '', request.query_param)
        request.query_param.clear()
        request.query_param.update(new_query_param)
        request.sorted_query_params()

        # Step 1: Construct the canonical request string
        canonical_query_string = "&".join(
            f"{percent_code(quote_plus(k))}={percent_code(quote_plus(str(v)))}"
            for k, v in request.query_param.items()
        )
        hashed_request_payload = sha256_hex(request.body or b'')
        request.headers['x-acs-content-sha256'] = hashed_request_payload

        if SECURITY_TOKEN:
            signature_request.headers["x-acs-security-token"] = SECURITY_TOKEN
        request.sorted_headers()

        filtered_headers = OrderedDict()
        for k, v in request.headers.items():
            if k.lower().startswith("x-acs-") or k.lower() in ["host", "content-type"]:
                filtered_headers[k.lower()] = v

        canonical_headers = "\n".join(f"{k}:{v}" for k, v in filtered_headers.items()) + "\n"
        signed_headers = ";".join(filtered_headers.keys())

        canonical_request = (
            f"{request.http_method}\n{request.canonical_uri}\n{canonical_query_string}\n"
            f"{canonical_headers}\n{signed_headers}\n{hashed_request_payload}"
        )
        print(canonical_request)

        # Step 2: Construct the string to sign
        hashed_canonical_request = sha256_hex(canonical_request.encode("utf-8"))
        string_to_sign = f"{ALGORITHM}\n{hashed_canonical_request}"
        print(string_to_sign)

        # Step 3: Calculate the signature
        signature = hmac256(ACCESS_KEY_SECRET.encode("utf-8"), string_to_sign).hex().lower()

        # Step 4: Construct the Authorization header
        authorization = f'{ALGORITHM} Credential={ACCESS_KEY_ID},SignedHeaders={signed_headers},Signature={signature}'
        request.headers["Authorization"] = authorization
    except Exception as e:
        print("Failed to get authorization")
        print(e)


def form_data_to_string(form_data: Dict[str, Any]) -> str:
    tile_map = OrderedDict()
    process_object(tile_map, "", form_data)
    return urlencode(tile_map)


def process_object(result_map: Dict[str, str], key: str, value: Any) -> None:
    if value is None:
        return

    if isinstance(value, (list, tuple)):
        for i, item in enumerate(value):
            process_object(result_map, f"{key}.{i + 1}", item)
    elif isinstance(value, dict):
        for sub_key, sub_value in value.items():
            process_object(result_map, f"{key}.{sub_key}", sub_value)
    else:
        key = key.lstrip(".")
        result_map[key] = value.decode("utf-8") if isinstance(value, bytes) else str(value)


def hmac256(key: bytes, msg: str) -> bytes:
    return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()


def sha256_hex(s: bytes) -> str:
    return hashlib.sha256(s).hexdigest()


def call_api(request: SignatureRequest) -> None:
    url = f"https://{request.host}{request.canonical_uri}"
    if request.query_param:
        url += "?" + urlencode(request.query_param, doseq=True, safe="*")

    headers = dict(request.headers)
    data = request.body

    try:
        response = requests.request(
            method=request.http_method, url=url, headers=headers, data=data
        )
        response.raise_for_status()
        print(response.text)
    except requests.RequestException as e:
        print("Failed to send request")
        print(e)


def percent_code(encoded_str: str) -> str:
    return encoded_str.replace("+", "%20").replace("*", "%2A").replace("%7E", "~")


# Get Access Key ID and Access Key Secret from environment variables
ACCESS_KEY_ID = os.environ.get("ALIBABA_CLOUD_ACCESS_KEY_ID")
ACCESS_KEY_SECRET = os.environ.get("ALIBABA_CLOUD_ACCESS_KEY_SECRET")
SECURITY_TOKEN = os.environ.get("ALIBABA_CLOUD_SECURITY_TOKEN")

ALGORITHM = "ACS3-HMAC-SHA256"

"""
Signature example. When testing, you can select an example from the main function and modify the example values as needed. For example, to call SendSms, select Example 1 and then modify http_method, host, x_acs_action, x_acs_version, and query_param.
The logic for getting the canonicalUri value is the only difference between ROA and RPC APIs.

Get the request method (methods), request parameter name (name), request parameter type (type), and request parameter location (in) from the OpenAPI metadata, and encapsulate the parameters into SignatureRequest.
1. If the request parameter is shown as "in":"query" in the metadata, pass the parameter using queryParam without setting content-type. Note: For RPC APIs, this type of parameter can also be passed through the body with content-type as application/x-www-form-urlencoded. See Example 3.
2. If the request parameter is shown as "in": "body" in the metadata, pass the parameter through the body and set content-type as needed. Note: For RPC APIs, it is not recommended to use application/json. You can use Example 3 instead.
3. If the request parameter is shown as "in": "formData" in the metadata, pass the parameter through the body with content-type as application/x-www-form-urlencoded.
"""
if __name__ == "__main__":
    # RPC API request example 1: Request parameters are in "query"
    http_method = "POST"  # Request method, which can be obtained from the metadata. POST is recommended.
    canonical_uri = "/"  # RPC APIs have no resource path, so use a forward slash (/) as the CanonicalURI
    host = "ecs.cn-hangzhou.aliyuncs.com"  # Cloud product endpoint
    x_acs_action = "DescribeInstanceStatus"  # API name
    x_acs_version = "2014-05-26"  # API version number
    signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
    # DescribeInstanceStatus request parameters:
    # RegionId is of type String in the metadata, "in":"query", required
    signature_request.query_param['RegionId'] = 'cn-hangzhou'
    # InstanceId is of type array in the metadata, "in":"query", not required
    signature_request.query_param['InstanceId'] = ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX",
                                                   "i-bp1incuofvzxXXXXXXXX"]

    # # RPC API request example 2: Request parameters are in "body" (file upload scenario)
    # http_method = "POST"
    # canonical_uri = "/"
    # host = "ocr-api.cn-hangzhou.aliyuncs.com"
    # x_acs_action = "RecognizeGeneral"
    # x_acs_version = "2021-07-07"
    # signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
    # # Request parameters are shown as "in": "body" in the metadata, passed through the body.
    # file_path = "D:\\test.png"
    # with open(file_path, 'rb') as file:
    #     # Read the image content as a byte array
    #     signature_request.body = file.read()
    #     signature_request.headers["content-type"] = "application/octet-stream"

    # # RPC API request example 3: Request parameters are in "formData" or "in":"body" (non-file upload scenario)
    # http_method = "POST"
    # canonical_uri = "/"
    # host = "mt.aliyuncs.com"
    # x_acs_action = "TranslateGeneral"
    # x_acs_version = "2018-10-12"
    # signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
    # # TranslateGeneral request parameters:
    # # Context is of type String in the metadata, "in":"query", not required
    # signature_request.query_param['Context'] = 'Morning'
    # # Parameters like FormatType, SourceLanguage, TargetLanguage are shown as "in":"formData" in the metadata
    # form_data = OrderedDict()
    # form_data["FormatType"] = "text"
    # form_data["SourceLanguage"] = "zh"
    # form_data["TargetLanguage"] = "en"
    # form_data["SourceText"] = "Hello"
    # form_data["Scene"] = "general"
    # signature_request.body = bytes(form_data_to_string(form_data), 'utf-8')
    # signature_request.headers["content-type"] = "application/x-www-form-urlencoded"

    # # Example 4: ROA API POST request
    # http_method = "POST"
    # canonical_uri = "/clusters"
    # host = "cs.cn-beijing.aliyuncs.com"
    # x_acs_action = "CreateCluster"
    # x_acs_version = "2015-12-15"
    # signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
    # Request parameters are shown as "in":"body" in the metadata, passed through the body.
    # body = OrderedDict()
    # body["name"] = "testDemo"
    # body["region_id"] = "cn-beijing"
    # body["cluster_type"] = "ExternalKubernetes"
    # body["vpcid"] = "vpc-2zeou1uod4ylaXXXXXXXX"
    # body["container_cidr"] = "172.16.1.0/20"
    # body["service_cidr"] = "10.2.0.0/24"
    # body["security_group_id"] = "sg-2ze1a0rlgeo7XXXXXXXX"
    # body["vswitch_ids"] = ["vsw-2zei30dhfldu8XXXXXXXX"]
    # signature_request.body = bytes(json.dumps(body, separators=(',', ':')), 'utf-8')
    # signature_request.headers["content-type"] = "application/json; charset=utf-8"

    # # Example 5: ROA API GET request
    # http_method = "GET"
    # # If canonicalUri has a path parameter, encode the path parameter: percent_code({path_parameter})
    # cluster_id_encode = percent_code("ca72cfced86db497cab79aa28XXXXXXXX")
    # canonical_uri = f"/clusters/{cluster_id_encode}/resources"
    # host = "cs.cn-beijing.aliyuncs.com"
    # x_acs_action = "DescribeClusterResources"
    # x_acs_version = "2015-12-15"
    # signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)
    # signature_request.query_param['with_addon_resources'] = True

    # # Example 6: ROA API DELETE request
    # http_method = "DELETE"
    # # If canonicalUri has a path parameter, encode the path parameter: percent_code({path_parameter})
    # cluster_id_encode = percent_code("ca72cfced86db497cab79aa28XXXXXXXX")
    # canonical_uri = f"/clusters/{cluster_id_encode}"
    # host = "cs.cn-beijing.aliyuncs.com"
    # x_acs_action = "DeleteCluster"
    # x_acs_version = "2015-12-15"
    # signature_request = SignatureRequest(http_method, canonical_uri, host, x_acs_action, x_acs_version)

    get_authorization(signature_request)
    call_api(signature_request)

Go example

Note

The example code runs in go1.22.2. You may need to adjust the code based on your specific situation.

Run the following command in the terminal:

go get github.com/google/uuid
go get golang.org/x/exp/maps
package main

import (
	"bytes"
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"io"
	"os"
	"sort"

	"golang.org/x/exp/maps"

	"fmt"
	"net/http"
	"net/url"
	"strings"
	"time"

	"github.com/google/uuid"
)

type Request struct {
	httpMethod   string
	canonicalUri string
	host         string
	xAcsAction   string
	xAcsVersion  string
	headers      map[string]string
	body         []byte
	queryParam   map[string]interface{}
}

func NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion string) *Request {
	req := &Request{
		httpMethod:   httpMethod,
		canonicalUri: canonicalUri,
		host:         host,
		xAcsAction:   xAcsAction,
		xAcsVersion:  xAcsVersion,
		headers:      make(map[string]string),
		queryParam:   make(map[string]interface{}),
	}
	req.headers["host"] = host
	req.headers["x-acs-action"] = xAcsAction
	req.headers["x-acs-version"] = xAcsVersion
	req.headers["x-acs-date"] = time.Now().UTC().Format(time.RFC3339)
	req.headers["x-acs-signature-nonce"] = uuid.New().String()
	return req
}

// os.Getenv() gets the AccessKey ID and AccessKey secret from environment variables.
var (
	AccessKeyId     = os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_ID")
	AccessKeySecret = os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")
	SecurityToken   = os.Getenv("ALIBABA_CLOUD_SECURITY_TOKEN")
	ALGORITHM       = "ACS3-HMAC-SHA256"
)

// Signature example. Replace the example parameters in the main method as needed.
// The logic for getting the canonicalUri value is the only difference between ROA and RPC APIs. The rest is similar.
// Get the request method (methods), request parameter name (name), request parameter type (type), and request parameter location (in) from the API metadata, and encapsulate the parameters into SignatureRequest.
// 1. If the request parameter is shown as "in":"query" in the metadata, pass the parameter using queryParam. Note: For RPC APIs, this type of parameter can also be passed through the body with content-type as application/x-www-form-urlencoded. See Example 3.
// 2. If the request parameter is shown as "in": "body" in the metadata, pass the parameter through the body. The MIME type is application/octet-stream or application/json. For RPC APIs, it is not recommended to use application/json. You can use Example 3 instead.
// 3. If the request parameter is shown as "in": "formData" in the metadata, pass the parameter through the body. The MIME type is application/x-www-form-urlencoded.
func main() {
	// RPC API request example 1: Request parameters are in "query"
	httpMethod := "POST"                   // Request method. Most RPC APIs support both POST and GET. This example uses POST.
	canonicalUri := "/"                    // RPC APIs have no resource path, so use a forward slash (/) as the CanonicalURI
	host := "ecs.cn-hangzhou.aliyuncs.com" // Cloud product endpoint
	xAcsAction := "DescribeInstanceStatus" // API name
	xAcsVersion := "2014-05-26"            // API version number
	req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
	// DescribeInstanceStatus request parameters:
	// RegionId is of type String in the metadata, "in":"query", required
	req.queryParam["RegionId"] = "cn-hangzhou"
	// InstanceId is of type array in the metadata, "in":"query", not required
	instanceIds := []interface{}{"i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"}
	req.queryParam["InstanceId"] = instanceIds

	// // RPC API request example 2: Request parameters are in "body" (file upload scenario)
	// httpMethod := "POST"
	// canonicalUri := "/"
	// host := "ocr-api.cn-hangzhou.aliyuncs.com"
	// xAcsAction := "RecognizeGeneral"
	// xAcsVersion := "2021-07-07"
	// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
	// // Read file content
	// filePath := "D:\\test.png"
	// bytes, err := os.ReadFile(filePath)
	// if err != nil {
	//     fmt.Println("Error reading file:", err)
	//     return
	// }
	// req.body = bytes
	// req.headers["content-type"] = "application/octet-stream"

	// // RPC API request example 3: Request parameters are in "formData" or "in":"body" (non-file upload scenario)
	// httpMethod := "POST"
	// canonicalUri := "/"
	// host := "mt.aliyuncs.com"
	// xAcsAction := "TranslateGeneral"
	// xAcsVersion := "2018-10-12"
	// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
	// // TranslateGeneral request parameters:
	// // Context is of type String in the metadata, "in":"query", not required
	// req.queryParam["Context"] = "Morning"
	// // Parameters like FormatType, SourceLanguage, TargetLanguage are shown as "in":"formData" in the metadata
	// body := make(map[string]interface{})
	// body["FormatType"] = "text"
	// body["SourceLanguage"] = "zh"
	// body["TargetLanguage"] = "en"
	// body["SourceText"] = "Hello"
	// body["Scene"] = "general"
	// str := formDataToString(body)
	// req.body = []byte(*str)
	// req.headers["content-type"] = "application/x-www-form-urlencoded"

	// // ROA API POST request
	// httpMethod := "POST"
	// canonicalUri := "/clusters"
	// host := "cs.cn-beijing.aliyuncs.com"
	// xAcsAction := "CreateCluster"
	// xAcsVersion := "2015-12-15"
	// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
	// // Encapsulate request parameters. The request parameters are shown as "in": "body" in the metadata, which means the parameters are in the body.
	// body := make(map[string]interface{})
	// body["name"] = "testDemo"
	// body["region_id"] = "cn-beijing"
	// body["cluster_type"] = "ExternalKubernetes"
	// body["vpcid"] = "vpc-2zeou1uod4ylaXXXXXXXX"
	// body["container_cidr"] = "10.0.0.0/8"
	// body["service_cidr"] = "172.16.1.0/20"
	// body["security_group_id"] = "sg-2ze1a0rlgeo7XXXXXXXX"
	// vswitch_ids := []interface{}{"vsw-2zei30dhfldu8XXXXXXXX"}
	// body["vswitch_ids"] = vswitch_ids
	// jsonBytes, err := json.Marshal(body)
	// if err != nil {
	//     fmt.Println("Error marshaling to JSON:", err)
	//     return
	// }
	// req.body = []byte(jsonBytes)
	// req.headers["content-type"] = "application/json; charset=utf-8"

	// // ROA API GET request
	// httpMethod := "GET"
	// // If canonicalUri has a path parameter, encode the path parameter: percentCode({path_parameter})
	// canonicalUri := "/clusters/" + percentCode("c558c166928f9446dae400d106e124f66") + "/resources"
	// host := "cs.cn-beijing.aliyuncs.com"
	// xAcsAction := "DescribeClusterResources"
	// xAcsVersion := "2015-12-15"
	// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)
	// req.queryParam["with_addon_resources"] = "true"

	// // ROA API DELETE request
	// httpMethod := "DELETE"
	// // If canonicalUri has a path parameter, encode the path parameter: percentCode({path_parameter})
	// canonicalUri := "/clusters/" + percentCode("c558c166928f9446dae400d106e124f66")
	// host := "cs.cn-beijing.aliyuncs.com"
	// xAcsAction := "DeleteCluster"
	// xAcsVersion := "2015-12-15"
	// req := NewRequest(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion)

	// Signature process
	getAuthorization(req)
	// Call the API
	error := callAPI(req)
	if error != nil {
		println(error.Error())
	}
}

func callAPI(req *Request) error {
	urlStr := "https://" + req.host + req.canonicalUri
	q := url.Values{}
	keys := maps.Keys(req.queryParam)
	sort.Strings(keys)
	for _, k := range keys {
		v := req.queryParam[k]
		q.Set(k, fmt.Sprintf("%v", v))
	}
	urlStr += "?" + q.Encode()
	fmt.Println(urlStr)

	httpReq, err := http.NewRequest(req.httpMethod, urlStr, strings.NewReader(string(req.body)))
	if err != nil {
		return err
	}

	for key, value := range req.headers {
		httpReq.Header.Set(key, value)
	}

	client := &http.Client{}
	resp, err := client.Do(httpReq)
	if err != nil {
		return err
	}
	defer func(Body io.ReadCloser) {
		err := Body.Close()
		if err != nil {
			return
		}
	}(resp.Body)
	var respBuffer bytes.Buffer
	_, err = io.Copy(&respBuffer, resp.Body)
	if err != nil {
		return err
	}
	respBytes := respBuffer.Bytes()
	fmt.Println(string(respBytes))
	return nil
}

func getAuthorization(req *Request) {
	// Process parameters of List and Map types in queryParam, and flatten the parameters
	newQueryParams := make(map[string]interface{})
	processObject(newQueryParams, "", req.queryParam)
	req.queryParam = newQueryParams
	// Step 1: Construct the canonical request string
	canonicalQueryString := ""
	keys := maps.Keys(req.queryParam)
	sort.Strings(keys)
	for _, k := range keys {
		v := req.queryParam[k]
		canonicalQueryString += percentCode(url.QueryEscape(k)) + "=" + percentCode(url.QueryEscape(fmt.Sprintf("%v", v))) + "&"
	}
	canonicalQueryString = strings.TrimSuffix(canonicalQueryString, "&")
	fmt.Printf("canonicalQueryString========>%s\n", canonicalQueryString)

	var bodyContent []byte
	if req.body == nil {
		bodyContent = []byte("")
	} else {
		bodyContent = req.body
	}
	hashedRequestPayload := sha256Hex(bodyContent)
	req.headers["x-acs-content-sha256"] = hashedRequestPayload

	if SecurityToken != "" {
		req.headers["x-acs-security-token"] = SecurityToken
	}

	canonicalHeaders := ""
	signedHeaders := ""
	HeadersKeys := maps.Keys(req.headers)
	sort.Strings(HeadersKeys)
	for _, k := range HeadersKeys {
		lowerKey := strings.ToLower(k)
		if lowerKey == "host" || strings.HasPrefix(lowerKey, "x-acs-") || lowerKey == "content-type" {
			canonicalHeaders += lowerKey + ":" + req.headers[k] + "\n"
			signedHeaders += lowerKey + ";"
		}
	}
	signedHeaders = strings.TrimSuffix(signedHeaders, ";")

	canonicalRequest := req.httpMethod + "\n" + req.canonicalUri + "\n" + canonicalQueryString + "\n" + canonicalHeaders + "\n" + signedHeaders + "\n" + hashedRequestPayload
	fmt.Printf("canonicalRequest========>\n%s\n", canonicalRequest)

	// Step 2: Construct the string to sign
	hashedCanonicalRequest := sha256Hex([]byte(canonicalRequest))
	stringToSign := ALGORITHM + "\n" + hashedCanonicalRequest
	fmt.Printf("stringToSign========>\n%s\n", stringToSign)

	// Step 3: Calculate the signature
	byteData, err := hmac256([]byte(AccessKeySecret), stringToSign)
	if err != nil {
		fmt.Println(err)
		panic(err)
	}
	signature := strings.ToLower(hex.EncodeToString(byteData))

	// Step 4: Construct the Authorization header
	authorization := ALGORITHM + " Credential=" + AccessKeyId + ",SignedHeaders=" + signedHeaders + ",Signature=" + signature
	req.headers["Authorization"] = authorization
}

func hmac256(key []byte, toSignString string) ([]byte, error) {
	// Instantiate HMAC-SHA256 hash
	h := hmac.New(sha256.New, key)
	// Write the string to be signed
	_, err := h.Write([]byte(toSignString))
	if err != nil {
		return nil, err
	}
	// Calculate and return the signature
	return h.Sum(nil), nil
}

func sha256Hex(byteArray []byte) string {
	// Instantiate SHA-256 hash function
	hash := sha256.New()
	// Write the string to the hash function
	_, _ = hash.Write(byteArray)
	// Calculate the SHA-256 hash value and convert it to a lowercase hexadecimal string
	hexString := hex.EncodeToString(hash.Sum(nil))

	return hexString
}

func percentCode(str string) string {
	// Replace specific encoded characters
	str = strings.ReplaceAll(str, "+", "%20")
	str = strings.ReplaceAll(str, "*", "%2A")
	str = strings.ReplaceAll(str, "%7E", "~")
	return str
}

func formDataToString(formData map[string]interface{}) *string {
	tmp := make(map[string]interface{})
	processObject(tmp, "", formData)
	res := ""
	urlEncoder := url.Values{}
	for key, value := range tmp {
		v := fmt.Sprintf("%v", value)
		urlEncoder.Add(key, v)
	}
	res = urlEncoder.Encode()
	return &res
}

// processObject recursively processes an object, expanding complex objects (like Maps and Lists) into flat key-value pairs
func processObject(mapResult map[string]interface{}, key string, value interface{}) {
	if value == nil {
		return
	}

	switch v := value.(type) {
	case []interface{}:
		for i, item := range v {
			processObject(mapResult, fmt.Sprintf("%s.%d", key, i+1), item)
		}
	case map[string]interface{}:
		for subKey, subValue := range v {
			processObject(mapResult, fmt.Sprintf("%s.%s", key, subKey), subValue)
		}
	default:
		if strings.HasPrefix(key, ".") {
			key = key[1:]
		}
		if b, ok := v.([]byte); ok {
			mapResult[key] = string(b)
		} else {
			mapResult[key] = fmt.Sprintf("%v", v)
		}
	}
}

Node.js example

Note

The example code runs in Node.js v20.13.1. You may need to adjust the code based on your specific situation.

This example uses Node.js.

const crypto = require('crypto');
const fs = require('fs');

class Request {
    constructor(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion) {
        this.httpMethod = httpMethod;
        this.canonicalUri = canonicalUri || '/';
        this.host = host;
        this.xAcsAction = xAcsAction;
        this.xAcsVersion = xAcsVersion;
        this.headers = {};
        this.body = null;
        this.queryParam = {};
        this.initHeader();
    }

    initHeader() {
        const date = new Date();
        this.headers = {
            'host': this.host,
            'x-acs-action': this.xAcsAction,
            'x-acs-version': this.xAcsVersion,
            'x-acs-date': date.toISOString().replace(/\..+/, 'Z'),
            'x-acs-signature-nonce': crypto.randomBytes(16).toString('hex')
        }
    }
}

const ALGORITHM = 'ACS3-HMAC-SHA256';
const accessKeyId = process.env.ALIBABA_CLOUD_ACCESS_KEY_ID;
const accessKeySecret = process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET;
const securityToken = process.env.ALIBABA_CLOUD_SECURITY_TOKEN;
const encoder = new TextEncoder()

if (!accessKeyId || !accessKeySecret) {
    console.error('ALIBABA_CLOUD_ACCESS_KEY_ID and ALIBABA_CLOUD_ACCESS_KEY_SECRET environment variables must be set.');
    process.exit(1);
}

function getAuthorization(signRequest) {
    try {
        newQueryParam = {};
        processObject(newQueryParam, "", signRequest.queryParam);
        signRequest.queryParam = newQueryParam;
        // Step 1: Construct the canonical request string
        const canonicalQueryString = Object.entries(signRequest.queryParam)
            .sort(([a], [b]) => a.localeCompare(b))
            .map(([key, value]) => `${percentCode(key)}=${percentCode(value)}`)
            .join('&');

        // Request body. If the request body is empty, for example, in a GET request, RequestPayload is a fixed empty string.
        const requestPayload = signRequest.body || encoder.encode('');
        const hashedRequestPayload = sha256Hex(requestPayload);
        signRequest.headers['x-acs-content-sha256'] = hashedRequestPayload;
        if (securityToken) {
            signRequest.headers['x-acs-security-token'] = securityToken;
        }

        // Convert all keys to lowercase
        signRequest.headers = Object.fromEntries(
            Object.entries(signRequest.headers).map(([key, value]) => [key.toLowerCase(), value])
        );

        const sortedKeys = Object.keys(signRequest.headers)
            .filter(key => key.startsWith('x-acs-') || key === 'host' || key === 'content-type')
            .sort();
        // List of signed headers. Multiple request header names (lowercase) are sorted alphabetically and separated by semicolons (;).
        const signedHeaders = sortedKeys.join(";")
        // Construct request headers. Multiple canonical headers are concatenated after being sorted in ascending order by header name (lowercase).
        const canonicalHeaders = sortedKeys.map(key => `${key}:${signRequest.headers[key]}`).join('\n') + '\n';

        const canonicalRequest = [
            signRequest.httpMethod,
            signRequest.canonicalUri,
            canonicalQueryString,
            canonicalHeaders,
            signedHeaders,
            hashedRequestPayload
        ].join('\n');
        console.log('canonicalRequest=========>\n', canonicalRequest);

        // Step 2: Construct the string to sign
        const hashedCanonicalRequest = sha256Hex(encoder.encode(canonicalRequest));
        const stringToSign = `${ALGORITHM}\n${hashedCanonicalRequest}`;
        console.log('stringToSign=========>', stringToSign);

        // Step 3: Calculate the signature
        const signature = hmac256(accessKeySecret, stringToSign);
        console.log('signature=========>', signature);

        // Step 4: Construct the Authorization header
        const authorization = `${ALGORITHM} Credential=${accessKeyId},SignedHeaders=${signedHeaders},Signature=${signature}`;
        console.log('authorization=========>', authorization);
        signRequest.headers['Authorization'] = authorization;
    } catch (error) {
        console.error('Failed to get authorization');
        console.error(error);
    }
}

async function callApi(signRequest) {
    try {
        let url = `https://${signRequest.host}${signRequest.canonicalUri}`;
        // Add request parameters
        if (signRequest.queryParam) {
            const query = new URLSearchParams(signRequest.queryParam);
            url += '?' + query.toString();
        }
        console.log('url=========>', url);

        // Configure request options
        let options = {
            method: signRequest.httpMethod.toUpperCase(),
            headers: signRequest.headers
        };

        // Handle request body
        if (signRequest.body && ['POST', 'PUT'].includes(signRequest.httpMethod.toUpperCase())) {
            options.body = signRequest.body;
        }
        return (await fetch(url, options)).text();
    } catch (error) {
        console.error('Failed to send request:', error);
    }
}

function percentCode(str) {
    return encodeURIComponent(str)
        .replace(/\+/g, '%20')
        .replace(/\*/g, '%2A')
        .replace(/~/g, '%7E');
}

function hmac256(key, data) {
    const hmac = crypto.createHmac('sha256', key);
    hmac.update(data, 'utf8');
    return hmac.digest('hex').toLowerCase();
}

function sha256Hex(bytes) {
    const hash = crypto.createHash('sha256');
    const digest = hash.update(bytes).digest('hex');
    return digest.toLowerCase();
}

function formDataToString(formData) {
    const tmp = {};
    processObject(tmp, "", formData);
    let queryString = '';
    for (let [key, value] of Object.entries(tmp)) {
        if (queryString !== '') {
            queryString += '&';
        }
        queryString += encodeURIComponent(key) + '=' + encodeURIComponent(value);
    }
    return queryString;
}

function processObject(map, key, value) {
    // If the value is null, no further processing is needed
    if (value === null) {
        return;
    }
    if (key === null) {
        key = "";
    }

    // When the value is an Array, iterate through each element and process recursively
    if (Array.isArray(value)) {
        value.forEach((item, index) => {
            processObject(map, `${key}.${index + 1}`, item);
        });
    } else if (typeof value === 'object' && value !== null) {
        // When the value is an Object, iterate through each key-value pair and process recursively
        Object.entries(value).forEach(([subKey, subValue]) => {
            processObject(map, `${key}.${subKey}`, subValue);
        });
    } else {
        // For keys starting with ".", remove the leading "." to maintain key continuity
        if (key.startsWith('.')) {
            key = key.slice(1);
        }
        map[key] = String(value);
    }
}

/**
 * Signature example. Replace the example parameters in the main method as needed.
 * The logic for getting the canonicalUri value is the only difference between ROA and RPC APIs. The rest is similar.
 *
 * Get the request method (methods), request parameter name (name), request parameter type (type), and request parameter location (in) from the API metadata, and encapsulate the parameters into SignatureRequest.
 * 1. If the request parameter is shown as "in":"query" in the metadata, pass the parameter using queryParam. Note: For RPC APIs, this type of parameter can also be passed through the body with content-type as application/x-www-form-urlencoded. See Example 3.
 * 2. If the request parameter is shown as "in": "body" in the metadata, pass the parameter through the body. The MIME type is application/octet-stream or application/json. For RPC APIs, it is not recommended to use application/json. You can use Example 3 instead.
 * 3. If the request parameter is shown as "in": "formData" in the metadata, pass the parameter through the body. The MIME type is application/x-www-form-urlencoded.
 */

// RPC API request example 1: Request parameters are in "query"
const httpMethod = 'POST'; // Request method. Most RPC APIs support both POST and GET. This example uses POST.
const canonicalUri = '/'; // RPC APIs have no resource path, so use a forward slash (/) as the CanonicalURI
const host = 'ecs.cn-hangzhou.aliyuncs.com'; // Endpoint
const xAcsAction = 'DescribeInstanceStatus'; // API name
const xAcsVersion = '2014-05-26'; // API version number
const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
// DescribeInstanceStatus request parameters:
signRequest.queryParam = {
    // RegionId is of type String in the metadata, "in":"query", required
    RegionId: 'cn-hangzhou',
    // InstanceId is of type array in the metadata, "in":"query", not required
    InstanceId: ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"],
}


// // RPC API request example 2: Request parameters are in "body" (file upload scenario)
// const httpMethod = 'POST';
// const canonicalUri = '/';
// const host = 'ocr-api.cn-hangzhou.aliyuncs.com';
// const xAcsAction = 'RecognizeGeneral';
// const xAcsVersion = '2021-07-07';
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
// const filePath = 'D:\\test.png';
// const bytes = fs.readFileSync(filePath);
// // Request parameters are shown as "in": "body" in the metadata, which means the parameters are in the body.
// signRequest.body = bytes;
// signRequest.headers['content-type'] = 'application/octet-stream';


// // RPC API request example 3: Request parameters are in "formData" or "in":"body" (non-file upload scenario)
// const httpMethod = 'POST'; // Request method. Most RPC APIs support both POST and GET. This example uses POST.
// const canonicalUri = '/'; // RPC APIs have no resource path, so use a forward slash (/) as the CanonicalURI
// const host = 'mt.aliyuncs.com'; // Endpoint
// const xAcsAction = 'TranslateGeneral'; // API name
// const xAcsVersion = '2018-10-12'; // API version number
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
// // TranslateGeneral request parameters:
// // Context is of type String in the metadata, "in":"query", not required
// signRequest.queryParam["Context"] = "Morning";
// // Parameters like FormatType, SourceLanguage, TargetLanguage are shown as "in":"formData" in the metadata
// const formData = {
//     SourceLanguage: "zh",
//     TargetLanguage: "en",
//     FormatType: "text",
//     Scene: "general",
//     SourceText: 'Hello'
// }
// const str = formDataToString(formData)
// signRequest.body = encoder.encode(str);
// signRequest.headers['content-type'] = 'application/x-www-form-urlencoded';


// // ROA API POST request
// const httpMethod = 'POST';
// const canonicalUri = '/clusters';
// const host = 'cs.cn-beijing.aliyuncs.com';
// const xAcsAction = 'CreateCluster';
// const xAcsVersion = '2015-12-15';
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
// // Request parameters are shown as "in": "body" in the metadata, which means the parameters are in the body.
// const body = {
//     name: 'testDemo',
//     region_id: 'cn-beijing',
//     cluster_type: 'ExternalKubernetes',
//     vpcid: 'vpc-2zeou1uod4ylaf35teei9',
//     container_cidr: '10.0.0.0/8',
//     service_cidr: '172.16.3.0/20',
//     security_group_id: 'sg-2ze1a0rlgeo7dj37dd1q',
//     vswitch_ids: [
//         'vsw-2zei30dhfldu8ytmtarro'
//       ],
// }
// signRequest.body = encoder.encode(JSON.stringify(body));
// signRequest.headers['content-type'] = 'application/json';


// // ROA API GET request
// const httpMethod = 'GET';
// // If canonicalUri has a path parameter, encode the path parameter: percentCode({path_parameter})
// const canonicalUri = '/clusters/' + percentCode("c28c2615f8bfd466b9ef9a76c61706e96") + '/resources';
// const host = 'cs.cn-beijing.aliyuncs.com';
// const xAcsAction = 'DescribeClusterResources';
// const xAcsVersion = '2015-12-15';
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);
// signRequest.queryParam = {
//     with_addon_resources: true,
// }


// // ROA API DELETE request
// const httpMethod = 'DELETE';
// // If canonicalUri has a path parameter, encode the path parameter: percentCode({path_parameter})
// const canonicalUri = '/clusters/' + percentCode("c28c2615f8bfd466b9ef9a76c61706e96");
// const host = 'cs.cn-beijing.aliyuncs.com';
// const xAcsAction = 'DeleteCluster';
// const xAcsVersion = '2015-12-15';
// const signRequest = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion, xAcsVersion);

getAuthorization(signRequest);
// Call the API
callApi(signRequest).then(r => {
    console.log(r);
}).catch(error => {
    console.error(error);
});

PHP example

Note

The example code runs in PHP 7.4.33. You may need to adjust the code based on your specific situation.

<?php

class SignatureDemo
{
    // Encryption algorithm
    private $ALGORITHM;
    // Access Key ID
    private $AccessKeyId;
    // Access Key Secret
    private $AccessKeySecret;

    private $SecurityToken;

    public function __construct()
    {
        date_default_timezone_set('UTC'); // Set timezone to GMT
        $this->AccessKeyId = getenv('ALIBABA_CLOUD_ACCESS_KEY_ID'); // getenv() gets the Access Key ID of the RAM user from an environment variable
        $this->AccessKeySecret = getenv('ALIBABA_CLOUD_ACCESS_KEY_SECRET'); // getenv() gets the Access Key Secret of the RAM user from an environment variable
        $this->SecurityToken = getenv('ALIBABA_CLOUD_SECURITY_TOKEN');
        $this->ALGORITHM = 'ACS3-HMAC-SHA256'; // Set encryption algorithm
    }

    /**
     * Signature example. Replace the example parameters in the main method as needed.
     * The logic for getting the canonicalUri value is the only difference between ROA and RPC APIs. The rest is similar.
     *
     * Get the request method (methods), request parameter name (name), request parameter type (type), and request parameter location (in) from the API metadata, and encapsulate the parameters into SignatureRequest.
     * 1. If the request parameter is shown as "in":"query" in the metadata, pass the parameter using queryParam. Note: For RPC APIs, this type of parameter can also be passed through the body with content-type as application/x-www-form-urlencoded. See Example 3.
     * 2. If the request parameter is shown as "in": "body" in the metadata, pass the parameter through the body. The MIME type is application/octet-stream or application/json. For RPC APIs, it is not recommended to use application/json. You can use Example 3 instead.
     * 3. If the request parameter is shown as "in": "formData" in the metadata, pass the parameter through the body. The MIME type is application/x-www-form-urlencoded.
     */
    public function main()
    {
        // RPC API request example 1: Request parameters are in "query"
        $request = $this->createRequest('POST', '/', 'ecs.cn-hangzhou.aliyuncs.com', 'DescribeInstanceStatus', '2014-05-26');
        // DescribeInstanceStatus request parameters:
        $request['queryParam'] = [
            // RegionId is of type String in the metadata, "in":"query", required
            'RegionId' => 'cn-hangzhou',
            // InstanceId is of type array in the metadata, "in":"query", not required
            'InstanceId' => ["i-bp11ht4h2kdXXXXXXXX", "i-bp16maz3h3xgXXXXXXXX", "i-bp10r67hmslXXXXXXXX"]
        ];

        // // RPC API request example 2: Request parameters are in "body" (file upload scenario)
        // $request = $this->createRequest('POST', '/', 'ocr-api.cn-hangzhou.aliyuncs.com', 'RecognizeGeneral', '2021-07-07');
        // // Request parameters are shown as "in": "body" in the metadata, passed through the body.
        // $filePath = 'D:\\test.png';
        // // Use a file resource to pass the binary file
        // $fileResource = fopen($filePath, 'rb');
        // $request['body'] = stream_get_contents($fileResource); 
        // $request['headers']['content-type'] = 'application/octet-stream'; // Set Content-Type to application/octet-stream
        // // Close the file resource
        // fclose($fileResource);


        // // RPC API request example 3: Request parameters are in "formData" or "in":"body" (non-file upload scenario)
        // $request = $this->createRequest('POST', '/', 'mt.aliyuncs.com', 'TranslateGeneral', '2018-10-12');
        // // TranslateGeneral request parameters:
        // $request['queryParam'] = [
        //     // Context is of type String in the metadata, "in":"query", not required
        //     'Context' => 'Morning',
        // ];
        // $formData = [
        //     'FormatType' => 'text',
        //     'SourceLanguage' => 'zh',
        //     'TargetLanguage' => 'en',
        //     'SourceText' => 'Hello',
        //     'Scene' => 'general',
        // ];
        // $str = self::formDataToString($formData);
        // $request['body'] = $str;
        // $request['headers']['content-type'] = 'application/x-www-form-urlencoded';


        // // ROA API POST request
        // $request = $this->createRequest('POST', '/clusters', 'cs.cn-beijing.aliyuncs.com', 'CreateCluster', '2015-12-15');
        // $bodyData = [
        //     'name' => 'Test Cluster',
        //     'region_id' => 'cn-beijing',
        //     'cluster_type' => 'ExternalKubernetes',
        //     'vpcid' => 'vpc-2zeou1uod4ylaXXXXXXXX',
        //     'service_cidr' => '10.2.0.0/24',
        //     'security_group_id' => 'sg-2ze1a0rlgeo7XXXXXXXX',
        //     "vswitch_ids" => [
        //         "vsw-2zei30dhfldu8XXXXXXXX"
        //     ]
        // ];
        // $request['body'] = json_encode($bodyData, JSON_UNESCAPED_UNICODE);
        // $request['headers']['content-type'] = 'application/json; charset=utf-8'; 


        // // ROA API GET request
        // // If canonicalUri has a path parameter, encode the path parameter: rawurlencode({path_parameter})
        // $cluster_id = 'c930976b3b1fc4e02bc09831dXXXXXXXX';
        // $canonicalUri = sprintf("/clusters/%s/resources", rawurlencode($cluster_id));
        // $request = $this->createRequest('GET', $canonicalUri, 'cs.cn-beijing.aliyuncs.com', 'DescribeClusterResources', '2015-12-15');
        // $request['queryParam'] = [
        //     'with_addon_resources' => true,
        // ];


        // // ROA API DELETE request
        // $cluster_id = 'c930976b3b1fc4e02bc09831dXXXXXXXX';
        // $canonicalUri = sprintf("/clusters/%s", rawurlencode($cluster_id));
        // $request = $this->createRequest('DELETE', $canonicalUri, 'cs.cn-beijing.aliyuncs.com', 'DeleteCluster', '2015-12-15');

        $this->getAuthorization($request);
        // Call the API
        $this->callApi($request);
    }

    private function createRequest($httpMethod, $canonicalUri, $host, $xAcsAction, $xAcsVersion)
    {
        $headers = [
            'host' => $host,
            'x-acs-action' => $xAcsAction,
            'x-acs-version' => $xAcsVersion,
            'x-acs-date' => gmdate('Y-m-d\TH:i:s\Z'),
            'x-acs-signature-nonce' => bin2hex(random_bytes(16)),
        ];
        return [
            'httpMethod' => $httpMethod,
            'canonicalUri' => $canonicalUri,
            'host' => $host,
            'headers' => $headers,
            'queryParam' => [],
            'body' => null,
        ];
    }

    private function getAuthorization(&$request)
    {
        $request['queryParam'] = $this->processObject($request['queryParam']);
        $canonicalQueryString = $this->buildCanonicalQueryString($request['queryParam']);
        $hashedRequestPayload = hash('sha256', $request['body'] ?? '');
        $request['headers']['x-acs-content-sha256'] = $hashedRequestPayload;

        if($this->SecurityToken){
            $request['headers']['x-acs-security-token'] = $this->SecurityToken;
        }

        $canonicalHeaders = $this->buildCanonicalHeaders($request['headers']);
        $signedHeaders = $this->buildSignedHeaders($request['headers']);

        $canonicalRequest = implode("\n", [
            $request['httpMethod'],
            $request['canonicalUri'],
            $canonicalQueryString,
            $canonicalHeaders,
            $signedHeaders,
            $hashedRequestPayload,
        ]);

        $hashedCanonicalRequest = hash('sha256', $canonicalRequest);
        $stringToSign = "{$this->ALGORITHM}\n$hashedCanonicalRequest";

        $signature = strtolower(bin2hex(hash_hmac('sha256', $stringToSign, $this->AccessKeySecret, true)));
        $authorization = "{$this->ALGORITHM} Credential={$this->AccessKeyId},SignedHeaders=$signedHeaders,Signature=$signature";

        $request['headers']['Authorization'] = $authorization;
    }

    private function callApi($request)
    {
        try {
            // Send request via cURL
            $url = "https://" . $request['host'] . $request['canonicalUri'];

            // Add request parameters to the URL
            if (!empty($request['queryParam'])) {
                $url .= '?' . http_build_query($request['queryParam']);
            }

            echo $url;
            // Initialize cURL session
            $ch = curl_init();

            // Set cURL options
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Disable SSL certificate verification. Note that this reduces security and should not be used in a production environment. (Not recommended!!!)
            curl_setopt($ch, CURLOPT_URL, $url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // Return content instead of outputting it
            curl_setopt($ch, CURLOPT_HTTPHEADER, $this->convertHeadersToArray($request['headers'])); // Add request headers

            // Set cURL options based on request type
            switch ($request['httpMethod']) {
                case "GET":
                    break;
                case "POST":
                    curl_setopt($ch, CURLOPT_POST, true);
                    curl_setopt($ch, CURLOPT_POSTFIELDS, $request['body']);
                    break;
                case "DELETE":
                    curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE");
                    break;
                default:
                    echo "Unsupported HTTP method: " . $request['body'];
                    throw new Exception("Unsupported HTTP method");
            }

            // Send the request
            $result = curl_exec($ch);

            // Check for errors
            if (curl_errno($ch)) {
                echo "Failed to send request: " . curl_error($ch);
            } else {
                echo $result;
            }

        } catch (Exception $e) {
            echo "Error: " . $e->getMessage();
        } finally {
            // Close cURL session
            curl_close($ch);
        }
    }

    function formDataToString($formData)
    {
        $res = self::processObject($formData);
        return http_build_query($res);
    }

    function processObject($value)
    {
        // If the value is null, no further processing is needed
        if ($value === null) {
            return;
        }
        $tmp = [];
        foreach ($value as $k => $v) {
            if (0 !== strpos($k, '_')) {
                $tmp[$k] = $v;
            }
        }
        return self::flatten($tmp);
    }

    private static function flatten($items = [], $delimiter = '.', $prepend = '')
    {
        $flatten = [];
        foreach ($items as $key => $value) {
            $pos = \is_int($key) ? $key + 1 : $key;

            if (\is_object($value)) {
                $value = get_object_vars($value);
            }

            if (\is_array($value) && !empty($value)) {
                $flatten = array_merge(
                    $flatten,
                    self::flatten($value, $delimiter, $prepend . $pos . $delimiter)
                );
            } else {
                if (\is_bool($value)) {
                    $value = true === $value ? 'true' : 'false';
                }
                $flatten["$prepend$pos"] = $value;
            }
        }
        return $flatten;
    }


    private function convertHeadersToArray($headers)
    {
        $headerArray = [];
        foreach ($headers as $key => $value) {
            $headerArray[] = "$key: $value";
        }
        return $headerArray;
    }


    private function buildCanonicalQueryString($queryParams)
    {

        ksort($queryParams);
        // Build and encode query parameters
        $params = [];
        foreach ($queryParams as $k => $v) {
            if (null === $v) {
                continue;
            }
            $str = rawurlencode($k);
            if ('' !== $v && null !== $v) {
                $str .= '=' . rawurlencode($v);
            } else {
                $str .= '=';
            }
            $params[] = $str;
        }
        return implode('&', $params);
    }

    private function buildCanonicalHeaders($headers)
    {
        // Sort headers by key and concatenate them
        uksort($headers, 'strcasecmp');
        $canonicalHeaders = '';
        foreach ($headers as $key => $value) {
            $canonicalHeaders .= strtolower($key) . ':' . trim($value) . "\n";
        }
        return $canonicalHeaders;
    }

    private function buildSignedHeaders($headers)
    {
        // Build the signed headers string
        $signedHeaders = array_keys($headers);
        sort($signedHeaders, SORT_STRING | SORT_FLAG_CASE);
        return implode(';', array_map('strtolower', $signedHeaders));
    }
}

$demo = new SignatureDemo();
$demo->main();

.NET example

Note

The example code runs in .NET 8.0.302. You may need to adjust the code based on your specific situation.

using System.Globalization;
using System.Net;
using System.Net.Http.Headers;
using System.Security.Cryptography;
using System.Text;
using System.Web;
using Newtonsoft.Json;

namespace SignatureDemo
{
    public class Request
    {
        public string HttpMethod { get; private set; }
        public string CanonicalUri { get; private set; }
        public string Host { get; private set; }
        public string XAcsAction { get; private set; }
        public string XAcsVersion { get; private set; }
        public SortedDictionary<string, object> Headers { get; private set; }
        public byte[]? Body { get; set; }
        public Dictionary<string, object> QueryParam { get; set; }

        public Request(string httpMethod, string canonicalUri, string host, string xAcsAction, string xAcsVersion)
        {
            HttpMethod = httpMethod;
            CanonicalUri = canonicalUri;
            Host = host;
            XAcsAction = xAcsAction;
            XAcsVersion = xAcsVersion;
            Headers = [];
            QueryParam = [];
            Body = null;
            InitHeader();
        }

        private void InitHeader()
        {
            Headers["host"] = Host;
            Headers["x-acs-action"] = XAcsAction;
            Headers["x-acs-version"] = XAcsVersion;
            DateTime utcNow = DateTime.UtcNow;
            Headers["x-acs-date"] = utcNow.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture);
            Headers["x-acs-signature-nonce"] = Guid.NewGuid().ToString();
        }
    }

    public class Program
    {
        private static readonly string AccessKeyId = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ACCESS_KEY_ID") ?? throw new InvalidOperationException("The ALIBABA_CLOUD_ACCESS_KEY_ID environment variable is not set");
        private static readonly string AccessKeySecret = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_ACCESS_KEY_SECRET") ?? throw new InvalidOperationException("The ALIBABA_CLOUD_ACCESS_KEY_SECRET environment variable is not set");
        private static readonly string? SecurityToken = Environment.GetEnvironmentVariable("ALIBABA_CLOUD_SECURITY_TOKEN");
        private const string Algorithm = "ACS3-HMAC-SHA256";
        private const string ContentType = "content-type";

        /**
        * Signature example. Replace the example parameters in the main method as needed.
        * The logic for getting the canonicalUri value is the only difference between ROA and RPC APIs. The rest is similar.
        *
        * Get the request method (methods), request parameter name (name), request parameter type (type), and request parameter location (in) from the API metadata, and encapsulate the parameters into SignatureRequest.
        * 1. If the request parameter is shown as "in":"query" in the metadata, pass the parameter using queryParam. Note: For RPC APIs, this type of parameter can also be passed through the body with content-type as application/x-www-form-urlencoded. See Example 3.
        * 2. If the request parameter is shown as "in": "body" in the metadata, pass the parameter through the body. The MIME type is application/octet-stream or application/json. For RPC APIs, it is not recommended to use application/json. You can use Example 3 instead.
        * 3. If the request parameter is shown as "in": "formData" in the metadata, pass the parameter through the body. The MIME type is application/x-www-form-urlencoded.
        */
        public static void Main(string[] args)
        {
            // RPC API request example 1: Request parameters are in "query"
            string httpMethod = "POST"; // Request method. Most RPC APIs support both POST and GET. This example uses POST.
            string canonicalUri = "/"; // RPC APIs have no resource path, so use a forward slash (/) as the CanonicalURI
            string host = "ecs.cn-hangzhou.aliyuncs.com"; // Cloud product endpoint
            string xAcsAction = "DescribeInstanceStatus"; // API name
            string xAcsVersion = "2014-05-26"; // API version number
            var request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
            // DescribeInstanceStatus request parameters:
            // RegionId is of type String in the metadata, "in":"query", required
            request.QueryParam["RegionId"] = "cn-hangzhou"; 
            // InstanceId is of type array in the metadata, "in":"query", not required
            List<string> instanceIds = ["i-bp10igfmnyttXXXXXXXX", "i-bp1incuofvzxXXXXXXXX", "i-bp1incuofvzxXXXXXXXX"];
            request.QueryParam["InstanceId"] = instanceIds; 

            // // RPC API request example 2: Request parameters are in "body" (file upload scenario)
            // string httpMethod = "POST"; 
            // string canonicalUri = "/"; 
            // string host = "ocr-api.cn-hangzhou.aliyuncs.com"; 
            // string xAcsAction = "RecognizeGeneral"; 
            // string xAcsVersion = "2021-07-07"; 
            // var request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
            // // Request parameters are shown as "in": "body" in the metadata, passed through the body.
            // request.Body = File.ReadAllBytes(@"D:\test.png");
            // request.Headers["content-type"] = "application/octet-stream";


            // // RPC API request example 3: Request parameters are in "formData" or "in":"body" (non-file upload scenario)
            // string httpMethod = "POST"; 
            // string canonicalUri = "/"; 
            // string host = "mt.aliyuncs.com"; 
            // string xAcsAction = "TranslateGeneral"; 
            // string xAcsVersion = "2018-10-12"; 
            // var request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
            // // TranslateGeneral request parameters:
            // // Context is of type String in the metadata, "in":"query", not required
            // request.QueryParam["Context"] = "Morning"; 
            // // Parameters like FormatType, SourceLanguage, TargetLanguage are shown as "in":"formData" in the metadata
            // var body = new Dictionary<string, object>
            // {
            //     { "FormatType", "text" },
            //     { "SourceLanguage", "zh" },
            //     { "TargetLanguage", "en" },
            //     { "SourceText", "Hello" },
            //     { "Scene", "general" },
            // };
            // var str = FormDataToString(body);
            // request.Body = Encoding.UTF8.GetBytes(str);
            // request.Headers[ContentType] = "application/x-www-form-urlencoded";


            // // ROA API POST request
            // String httpMethod = "POST";
            // String canonicalUri = "/clusters";
            // String host = "cs.cn-beijing.aliyuncs.com";
            // String xAcsAction = "CreateCluster"; 
            // String xAcsVersion = "2015-12-15"; 
            // Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
            // // Request body. Convert the body to a JSON string using JsonConvert.
            // var body = new SortedDictionary<string, object>
            // {
            //     { "name", "testDemo" },
            //     { "region_id", "cn-beijing" },
            //     { "cluster_type", "ExternalKubernetes" },
            //     { "vpcid", "vpc-2zeou1uod4ylaXXXXXXXX" },
            //     { "container_cidr", "10.0.0.0/8" },
            //     { "service_cidr", "172.16.1.0/20" },
            //     { "security_group_id", "sg-2ze1a0rlgeo7XXXXXXXX" },
            //     { "vswitch_ids", new List<string>{"vsw-2zei30dhfldu8XXXXXXXX"} },
            // };
            // string jsonBody = JsonConvert.SerializeObject(body, Formatting.None);
            // request.Body = Encoding.UTF8.GetBytes(jsonBody);
            // request.Headers[ContentType] = "application/json; charset=utf-8";

            // // ROA API GET request
            // String httpMethod = "GET";
            // // If canonicalUri has a path parameter, encode the path parameter: PercentCode({path_parameter})
            // String canonicalUri = "/clusters/" + PercentCode("c81d501a467594eab873edbf2XXXXXXXX") + "/resources";
            // String host = "cs.cn-beijing.aliyuncs.com"; 
            // String xAcsAction = "DescribeClusterResources"; 
            // String xAcsVersion = "2015-12-15"; 
            // Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);
            // request.QueryParam["with_addon_resources"]=true;

            // // ROA API DELETE request
            // String httpMethod = "DELETE";
            // // If canonicalUri has a path parameter, encode the path parameter: PercentCode({path_parameter})
            // String canonicalUri = "/clusters/" + PercentCode("c81d501a467594eab873edbf2XXXXXXXX");
            // String host = "cs.cn-beijing.aliyuncs.com"; 
            // String xAcsAction = "DeleteCluster"; 
            // String xAcsVersion = "2015-12-15"; 
            // Request request = new Request(httpMethod, canonicalUri, host, xAcsAction, xAcsVersion);

            GetAuthorization(request);
            // Call the API
            var result = CallApiAsync(request);
            Console.WriteLine($"result:{result.Result}");
        }

        private static async Task<string?> CallApiAsync(Request request)
        {
            try
            {
                // Declare httpClient
                using var httpClient = new HttpClient();

                // Build the URL
                string url = $"https://{request.Host}{request.CanonicalUri}";
                var uriBuilder = new UriBuilder(url);
                var query = new List<string>();

                // Add request parameters
                foreach (var entry in request.QueryParam.OrderBy(e => e.Key.ToLower()))
                {
                    string value = entry.Value?.ToString() ?? "";
                    query.Add($"{entry.Key}={Uri.EscapeDataString(value)}");
                }

                uriBuilder.Query = string.Join("&", query);
                Console.WriteLine(uriBuilder.Uri);
                var requestMessage = new HttpRequestMessage
                {
                    Method = new HttpMethod(request.HttpMethod),
                    RequestUri = uriBuilder.Uri,
                };

                // Set request headers
                foreach (var entry in request.Headers)
                {
                    if (entry.Key == "Authorization")
                    {
                        requestMessage.Headers.TryAddWithoutValidation("Authorization", entry.Value.ToString()); ;
                    }
                    else if (entry.Key == ContentType) // Must be consistent with the definition in main
                    {
                        continue;
                    }
                    else
                    {
                        requestMessage.Headers.Add(entry.Key, entry.Value.ToString());
                    }
                }

                if (request.Body != null)
                {
                    HttpContent content = new ByteArrayContent(request.Body);
                    string contentType = request.Headers["content-type"].ToString();
                    content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType);
                    requestMessage.Content = content;
                }
                
                // Send the request
                HttpResponseMessage response = await httpClient.SendAsync(requestMessage);
                // Read the response content
                string result = await response.Content.ReadAsStringAsync();
                return result;
            }
            catch (UriFormatException e)
            {
                Console.WriteLine("Invalid URI syntax");
                Console.WriteLine(e.Message);
                return null;
            }
            catch (Exception e)
            {
                Console.WriteLine("Failed to send request");
                Console.WriteLine(e);
                return null;
            }
        }

        private static void GetAuthorization(Request request)
        {
            try
            {
                // Process parameters of List and Map types in queryParam, and flatten the parameters
                request.QueryParam = FlattenDictionary(request.QueryParam);

                // Step 1: Construct the canonical request string
                StringBuilder canonicalQueryString = new();
                foreach (var entry in request.QueryParam.OrderBy(e => e.Key.ToLower()))
                {
                    if (canonicalQueryString.Length > 0)
                    {
                        canonicalQueryString.Append('&');
                    }
                    canonicalQueryString.Append($"{PercentCode(entry.Key)}={PercentCode(entry.Value?.ToString() ?? "")}");
                }

                byte[] requestPayload = request.Body ?? Encoding.UTF8.GetBytes("");
                string hashedRequestPayload = Sha256Hash(requestPayload);
                request.Headers["x-acs-content-sha256"] = hashedRequestPayload;
                if (!string.IsNullOrEmpty(SecurityToken))
                {
                    request.Headers["x-acs-security-token"] = SecurityToken;
                }

                StringBuilder canonicalHeaders = new();
                StringBuilder signedHeadersSb = new();
                foreach (var entry in request.Headers.OrderBy(e => e.Key.ToLower()))
                {
                    if (entry.Key.StartsWith("x-acs-", StringComparison.CurrentCultureIgnoreCase) || entry.Key.Equals("host", StringComparison.OrdinalIgnoreCase) || entry.Key.Equals(ContentType, StringComparison.OrdinalIgnoreCase))
                    {
                        string lowerKey = entry.Key.ToLower();
                        string value = (entry.Value?.ToString() ?? "").Trim();
                        canonicalHeaders.Append($"{lowerKey}:{value}\n");
                        signedHeadersSb.Append($"{lowerKey};");
                    }
                }
                string signedHeaders = signedHeadersSb.ToString().TrimEnd(';');
                string canonicalRequest = $"{request.HttpMethod}\n{request.CanonicalUri}\n{canonicalQueryString}\n{canonicalHeaders}\n{signedHeaders}\n{hashedRequestPayload}";
                Console.WriteLine($"canonicalRequest:{canonicalRequest}");

                // Step 2: Construct the string to sign
                string hashedCanonicalRequest = Sha256Hash(Encoding.UTF8.GetBytes(canonicalRequest));
                string stringToSign = $"{Algorithm}\n{hashedCanonicalRequest}";
                Console.WriteLine($"stringToSign:{stringToSign}");

                // Step 3: Calculate the signature
                string signature = HmacSha256(AccessKeySecret, stringToSign);

                // Step 4: Construct the Authorization header
                string authorization = $"{Algorithm} Credential={AccessKeyId},SignedHeaders={signedHeaders},Signature={signature}";
                request.Headers["Authorization"] = authorization;
                Console.WriteLine($"authorization:{authorization}");
            }
            catch (Exception ex)
            {
                Console.WriteLine("Failed to get authorization");
                Console.WriteLine(ex.Message);
            }
        }

        private static string FormDataToString(Dictionary<string, object> formData)
        {
            Dictionary<string, object> tileMap = FlattenDictionary( formData);
            
            StringBuilder result = new StringBuilder();
            bool first = true;
            string symbol = "&";

            foreach (var entry in tileMap)
            {
                string value = entry.Value?.ToString() ?? "";
                if (!string.IsNullOrEmpty(value))
                {
                    if (!first)
                    {
                        result.Append(symbol);
                    }
                    first = false;
                    result.Append(PercentCode(entry.Key));
                    result.Append("=");
                    result.Append(PercentCode(value));
                }
            }
            return result.ToString();
        }

        private static Dictionary<string, object> FlattenDictionary(Dictionary<string, object> dictionary, string prefix = "")
        {
            var result = new Dictionary<string, object>();
            foreach (var kvp in dictionary)
            {
                string key = string.IsNullOrEmpty(prefix) ? kvp.Key : $"{prefix}.{kvp.Key}";

                if (kvp.Value is Dictionary<string, object> nestedDict)
                {
                    var nestedResult = FlattenDictionary(nestedDict, key);
                    foreach (var nestedKvp in nestedResult)
                    {
                        result[nestedKvp.Key] = nestedKvp.Value;
                    }
                }
                else if (kvp.Value is List<string> list)
                {
                    for (int i = 0; i < list.Count; i++)
                    {
                        result[$"{key}.{i + 1}"] = list[i];
                    }
                }
                else
                {
                    result[key] = kvp.Value;
                }
            }
            return result;
        }

        private static string HmacSha256(string key, string message)
        {
            using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key)))
            {
                byte[] hashMessage = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
                return BitConverter.ToString(hashMessage).Replace("-", "").ToLower();
            }
        }

        private static string Sha256Hash(byte[] input)
        {
            byte[] hashBytes = SHA256.HashData(input);
            return BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
        }

        private static string PercentCode(string str)
        {
            if (string.IsNullOrEmpty(str))
            {
                throw new ArgumentException("Input string cannot be null or empty");
            }
            return Uri.EscapeDataString(str).Replace("+", "%20").Replace("*", "%2A").Replace("%7E", "~");
        }
    }
}

Rust example

Note

The example code runs in rustc 1.82.0. You may need to adjust the code based on your specific situation.

To run the Rust example, add the following dependencies to your Cargo.toml file.

[dependencies]
serde = { version = "1.0" }
serde_json = "1.0"
rand = "0.8"
base64 = "0.21"
sha2 = "0.10"
chrono = "0.4"
hmac = "0.12"
hex = "0.4"
reqwest = { version = "0.11", features = ["json"] }
tokio = { version = "1", features = ["full"] }
percent-encoding = "2.1"
use core::str;
use std::collections::{BTreeMap, HashMap};
use std::env;
use std::time::{SystemTime, SystemTimeError};
use chrono::DateTime;
use hmac::{Hmac, Mac};
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
use rand::Rng;
use serde_json::{json, Value}; 
use std::borrow::Cow;    
use reqwest::{
    Client,
    header::{HeaderMap, HeaderValue}, Method, Response, StatusCode,
};
use sha2::{Digest, Sha256};
use base64::engine::general_purpose::STANDARD;
use base64::Engine;


// Generate x-acs-date
pub fn current_timestamp() -> Result<u64, SystemTimeError> {
    Ok(SystemTime::now()
        .duration_since(SystemTime::UNIX_EPOCH)?
        .as_secs())
}
// URL encoding
pub fn percent_code(encode_str: &str) -> Cow<'_, str> {
    let encoded = utf8_percent_encode(encode_str, NON_ALPHANUMERIC)
        .to_string()
        .replace("+", "20%")
        .replace("%5F", "_")
        .replace("%2D", "-")
        .replace("%2E", ".")
        .replace("%7E", "~");
        
    Cow::Owned(encoded) // Returns a Cow<str> that can hold a String or &str
}

fn flatten_target_ops(
    targets: Vec<HashMap<&str, &str>>,
    base_key: &str,
) -> Vec<(&'static str, &'static str)> {
    let mut result = Vec::new();

    for (idx, item) in targets.iter().enumerate() {
        let prefix = format!("{}.{}", base_key, idx + 1);

        for (&k, &v) in item {
            let key = format!("{}.{}", prefix, k);
            let key_static: &'static str = Box::leak(key.into_boxed_str());
            let value_static: &'static str = Box::leak(v.to_string().into_boxed_str());

            result.push((key_static, value_static));
        }
    }

    result
}

/// Calculate SHA-256 hash
pub fn sha256_hex(message: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(message);
    format!("{:x}", hasher.finalize()).to_lowercase()
}
// HMAC-SHA256
pub fn hmac256(key: &[u8], message: &str) -> Result<Vec<u8>, String> {
    let mut mac = Hmac::<Sha256>::new_from_slice(key)
        .map_err(|e| format!("use data key on sha256 fail:{}", e))?;
    mac.update(message.as_bytes());
    let signature = mac.finalize();
    Ok(signature.into_bytes().to_vec())
}
// Generate a unique random number for the signature
pub fn generate_random_string(length: usize) -> String {
    const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
    let mut rng = rand::thread_rng();
    (0..length)
        .map(|_| CHARSET[rng.gen_range(0..CHARSET.len())] as char)
        .collect()
}
pub fn generate_nonce() -> String {
    generate_random_string(32)
}
// Construct a canonical query string (encoded)
pub fn build_sored_encoded_query_string(query_params: &[(&str, &str)]) -> String {
    let sorted_query_params: BTreeMap<_, _> = query_params.iter().copied().collect();
    let encoded_params: Vec<String> = sorted_query_params
        .into_iter()
        .map(|(k, v)| {
            let encoded_key = percent_code(k);
            let encoded_value = percent_code(v);
            format!("{}={}", encoded_key, encoded_value)
        })
        .collect();
    encoded_params.join("&")
}
// Read the response
pub async fn read_response(result: Response) -> Result<(StatusCode, String), String> {
    let status = result.status();
    let data = result.bytes().await.map_err(|e| format!("Read response body failed: {}", e))?;
    let res = match str::from_utf8(&data) {
        Ok(s) => s.to_string(),
        Err(_) => return Err("Body contains non UTF-8 characters".to_string()),
    };
    Ok((status, res))
}
// Define the value type for FormData
#[derive(Debug, Clone)]
pub enum FormValue {
    String(String),
    Vec(Vec<String>),
    HashMap(HashMap<String, String>),
}
// Define a request body enumeration to uniformly handle request body types, including Json, Binary, and FormData.
pub enum RequestBody {
    Json(HashMap<String, Value>), // Json
    Binary(Vec<u8>), // Binary
    FormData(HashMap<String, FormValue>), //  FormData 
    None,
}
// Canonical request
pub async fn call_api(
    client: Client,
    method: Method,
    host: &str,
    canonical_uri: &str,
    query_params: &[(&str, &str)], 
    action: &str,
    version: &str,
    body: RequestBody,   
    access_key_id: &str,
    access_key_secret: &str,
) -> Result<String, String> {

    // Process the request body content based on the body type and store the processed content in the body_content variable.
    let body_content = match &body { 
        RequestBody::Json(body_map) => json!(body_map).to_string(),  
        RequestBody::Binary(binary_data) => {
            STANDARD.encode(binary_data)
        },
        RequestBody::FormData(form_data) => {
            let params: Vec<String> = form_data
            .iter()
            .flat_map(|(k, v)| {
                match v {
                    FormValue::String(s) => {
                        vec![format!("{}={}", percent_code(k), percent_code(&s))]
                    },
                    FormValue::Vec(vec) => {
                        vec.iter()
                            .map(|s| format!("{}={}", percent_code(k), percent_code(s)))
                            .collect::<Vec<_>>()
                    },
                    FormValue::HashMap(map) => {
                        map.iter()
                            .map(|(sk, sv)| format!("{}={}", percent_code(sk), percent_code(sv)))
                            .collect::<Vec<_>>()
                    },
                }
            })
            .collect();
            params.join("&") 
        },
        RequestBody::None => String::new(),
    };
    
    // Calculate x-acs-content-sha256 for the request body; prepare x-acs-date, x-acs-signature-nonce, and the request headers to be signed.
    let hashed_request_payload = if body_content.is_empty() {
        sha256_hex("") 
    } else {
        sha256_hex(&body_content) 
    };
    // x-acs-date
    let now_time = current_timestamp().map_err(|e| format!("Get current timestamp failed: {}", e))?;
    let datetime = DateTime::from_timestamp(now_time as i64, 0).ok_or_else(|| format!("Get datetime from timestamp failed: {}", now_time))?;
    let datetime_str = datetime.format("%Y-%m-%dT%H:%M:%SZ").to_string();
    // x-acs-signature-nonce
    let signature_nonce = generate_nonce();
    println!("Signature Nonce: {}", signature_nonce);
    // Request headers to be signed
    let sign_header_arr = &[
        "host",
        "x-acs-action",
        "x-acs-content-sha256",
        "x-acs-date",
        "x-acs-signature-nonce",
        "x-acs-version",
    ];
    let sign_headers = sign_header_arr.join(";");
    // 1. Construct canonical request headers
    let mut headers = HeaderMap::new();
    headers.insert("Host", HeaderValue::from_str(host).unwrap());
    headers.insert("x-acs-action", HeaderValue::from_str(action).unwrap());
    headers.insert("x-acs-version", HeaderValue::from_str(version).unwrap());
    headers.insert("x-acs-date", HeaderValue::from_str(&datetime_str).unwrap());
    headers.insert("x-acs-signature-nonce", HeaderValue::from_str(&signature_nonce).unwrap());
    headers.insert("x-acs-content-sha256", HeaderValue::from_str(&hashed_request_payload).unwrap());
    // 2. Construct the request headers to be signed
    let canonical_query_string = build_sored_encoded_query_string(query_params); // Encode and concatenate parameters
    println!("CanonicalQueryString: {}", canonical_query_string);
    let canonical_request = format!(
        "{}\n{}\n{}\n{}\n\n{}\n{}",
        method.as_str(),
        canonical_uri,
        canonical_query_string,
        sign_header_arr.iter().map(|&header| format!("{}:{}", header, headers[header].to_str().unwrap())).collect::<Vec<_>>().join("\n"),
        sign_headers,
        hashed_request_payload
    );
    println!("Canonical Request: {}", canonical_request);
    // 3. Calculate the SHA-256 hash of the request headers to be signed;
    let result = sha256_hex(&canonical_request);
    // 4. Construct the string to sign
    let string_to_sign = format!("ACS3-HMAC-SHA256\n{}", result);
    // 5. Calculate the signature
    let signature = hmac256(access_key_secret.as_bytes(), &string_to_sign)?;
    let data_sign = hex::encode(&signature);
    let auth_data = format!(
        "ACS3-HMAC-SHA256 Credential={},SignedHeaders={},Signature={}",
        access_key_id, sign_headers, data_sign
    );
    // 6. Construct the Authorization header
    headers.insert("Authorization", HeaderValue::from_str(&auth_data).unwrap());
    // Construct the URL and concatenate request parameters
    let url: String;
    if !query_params.is_empty() {
        url = format!("https://{}{}?{}", host, canonical_uri,canonical_query_string);
    } else {
        url = format!("https://{}{}", host, canonical_uri);
    }        
    // Call to send the request
    let response = send_request(
        &client,
        method,
        &url,
        headers,
        query_params,                
        &body,                      
        &body_content,                
    ) 
    .await?;
    
    // Read the response
    let (_, res) = read_response(response).await?;
    Ok(res)
}

/// Send the request
async fn send_request(
    client: &Client,
    method: Method,
    url: &str,
    headers: HeaderMap,
    query_params: &[(&str, &str)],     // Receive query parameters
    body: &RequestBody,                // Used to determine the body data type
    body_content: &str,                // Receive body request parameters (FormData/Json/Binary) when body is not empty
) -> Result<Response, String> {
    let mut request_builder = client.request(method.clone(), url);
    // Add request headers
    for (k, v) in headers.iter() {
        request_builder = request_builder.header(k, v.clone());
    }
     // Add request body
     match body {
        RequestBody::Binary(_) => {
            request_builder = request_builder.header("Content-Type", "application/octet-stream");
            request_builder = request_builder.body(body_content.to_string()); // Move the value here
        }
        RequestBody::Json(_) => {
            // If the body is a map and not empty, convert it to JSON, store it in the body_content variable, and set application/json; charset=utf-8
            if !body_content.is_empty() { 
                request_builder = request_builder.body(body_content.to_string());
                request_builder = request_builder.header("Content-Type", "application/json; charset=utf-8");
            }
        }
        RequestBody::FormData(_) => {
            // Handle form-data type, set content-type
            if !body_content.is_empty() { 
            request_builder = request_builder.header("Content-Type", "application/x-www-form-urlencoded");
            request_builder = request_builder.body(body_content.to_string());
            }
        }
        RequestBody::None => {
            request_builder = request_builder.body(String::new());
        }
    }
    // Build the request
    let request = request_builder
        .build()
        .map_err(|e| format!("build request fail: {}", e))?;
    // Send the request
    let response = client
        .execute(request)
        .await
        .map_err(|e| format!("execute request fail: {}", e))?;
    // Return the result
    Ok(response)
}


 /**
     * 
     * Signature example. Replace the example parameters in the main method as needed.
     * <p>
     * Get the request method (methods), request parameter name (name), request parameter type (type), and request parameter location (in) from the API metadata.
     * 1. If the request parameter is shown as "in":"query" in the metadata, pass the parameter using query_params. Note: For RPC APIs, this type of parameter can also be passed through the body with content-type as application/x-www-form-urlencoded. See Example 3.
     * 2. If the request parameter is shown as "in": "body" in the metadata, pass the parameter through the body. The MIME type is application/octet-stream or application/json. For RPC APIs, it is not recommended to use application/json. You can use Example 3 instead.
     * 2. If the request parameter is shown as "in": "formData" in the metadata, pass the parameter through the body. The MIME type is application/x-www-form-urlencoded.
*/
#[tokio::main]
async fn main() {
    // Create an HTTP client
    let client = Client::new();
    // env::var() gets the Access Key ID and Access Key Secret from environment variables
    let access_key_id = env::var("ALIBABA_CLOUD_ACCESS_KEY_ID").expect("Cannot get access key id.");
    let access_key_secret = env::var("ALIBABA_CLOUD_ACCESS_KEY_SECRET").expect("Cannot get access key id.");
    let access_key_id: &str = &access_key_id;
    let access_key_secret: &str = &access_key_secret;
    
    // RPC API request example 1: Request parameters are in "query"   POST
    let method = Method::POST; // Request method
    let host = "ecs.cn-hangzhou.aliyuncs.com"; // Endpoint
    let canonical_uri = "/"; // RPC APIs have no resource path, so use a forward slash (/) as the CanonicalURI
    let action = "DescribeInstanceStatus"; // API name
    let version = "2014-05-26"; // API version number
    let region_id = "cn-hangzhou";
    let instance_ids = vec![
        "i-bp11ht4XXXXXXXX",
        "i-bp16mazXXXXXXXX",
    ];
    let mut query: Vec<(&str, &str)> = Vec::new();
    query.push(("RegionId", region_id));
    for (index, instance_id) in instance_ids.iter().enumerate() {
        let key = format!("InstanceId.{}", index + 1); 
        query.push((Box::leak(key.into_boxed_str()), instance_id)); 
    }
    // Query parameters
    let query_params: &[(&str, &str)] = &query;
    // When the request body is empty
    let body = RequestBody:: None;
    
    // RPC API "in":"query" with complex query parameters  POST
    // let method = Method::POST; // Request method
    // let host = "tds.cn-shanghai.aliyuncs.com"; // Endpoint
    // let canonical_uri = "/"; // RPC APIs have no resource path, so use a forward slash (/) as the CanonicalURI
    // let action = "AddAssetSelectionCriteria"; // API name
    // let version = "2018-12-03"; // API version number
    // Define parameters
    // let mut target_op = HashMap::new();
    // target_op.insert("Operation", "add");
    // target_op.insert("Target", "i-2ze1j7ocdXXXXXXXX");
    // Define parameter TargetOperationList, passing a map type into the collection
    // let target_operation_list = vec![target_op];
    // Flatten parameters
    // let mut query = flatten_target_ops(target_operation_list, "TargetOperationList");
    // Normal parameters
    // query.push(("SelectionKey", "85a561b7-27d5-47ad-a0ec-XXXXXXXX"));
    // let query_params: &[(&str, &str)] = &query;
    // let body = RequestBody:: None;
    
    // RPC API request example 2: Request parameters are in "body" (file upload scenario)  POST
    // let method = Method::POST; // Request method
    // let host = "ocr-api.cn-hangzhou.aliyuncs.com";
    // let canonical_uri = "/";
    // let action = "RecognizeGeneral";
    // let version = "2021-07-07";
    // Request parameter "in":"body" binary file type 
    // let binary_data = std::fs::read("<FILE_PATH>").expect("Failed to read file"); // Replace <FILE_PATH> with the actual file path
    // When body is of binary type
    // let body = RequestBody::Binary(binary_data);
    // Query parameters are empty
    // let query_params = &[];
 
    // RPC API request example 3: Request parameters are in "formData" or "in":"body" (non-file upload scenario)  POST
    // let method = Method::POST; // Request method
    // let host = "mt.aliyuncs.com";
    // let canonical_uri = "/";
    // let action = "TranslateGeneral";
    // let version = "2018-10-12";
    // // Parameters like FormatType, SourceLanguage, TargetLanguage are shown as "in":"formData" in the metadata
    // let mut form_data = HashMap::new();  // body type is FormData(HashMap<String, FormValue>). FormValue supports Vec<String>, HashSet<String>, or HashMap<String, String>, etc. More types can be added in the FormValue enum.
    // form_data.insert(String::from("FormatType"),FormValue::String(String::from("text")));
    // form_data.insert(String::from("SourceLanguage"),FormValue::String(String::from("zh")));
    // form_data.insert(String::from("TargetLanguage"),FormValue::String(String::from("en")));
    // form_data.insert(String::from("SourceText"),FormValue::String(String::from("Hello")));
    // form_data.insert(String::from("Scene"),FormValue::String(String::from("general")));
    // // Query parameters
    // let query_params = &[("Context", "Morning")];
    // // When body is of FormData type, "in":"formdata"
    // let body = RequestBody::FormData(form_data);

    // ROA API POST request  API: CreateCluster  
    // Define API request constants
    // let method = Method::POST; // Request method
    // let host = "cs.cn-hangzhou.aliyuncs.com";
    // let canonical_uri = "/clusters";
    // let action = "CreateCluster";
    // let version = "2015-12-15";
    // // Set request body parameters
    // let mut body_json = HashMap::new();  //  body type is Json(HashMap<String, Value>). Value supports types: Value::String("test".to_string()) // String, Value::Number(serde_json::Number::from(42)) // Number, Value::Bool(true) // Boolean, Value::Null // Null, Value::Array(vec![Value::from(1), Value::from(2), Value::from(3)]) //Array, json!({"nested_key": "nested_value"})
    // body_json.insert(String::from("name"),json!("Test Cluster"));
    // body_json.insert(String::from("region_id"),json!("cn-hangzhou"));
    // body_json.insert(String::from("cluster_type"),json!("ExternalKubernetes"));
    // body_json.insert(String::from("vpcid"),json!("vpc-2zeou1uodXXXXXXXX"));
    // body_json.insert(String::from("container_cidr"),json!("10.X.X.X/X"));
    // body_json.insert(String::from("service_cidr"),json!("10.X.X.X/X"));
    // body_json.insert(String::from("security_group_id"),json!("sg-2ze1a0rlgXXXXXXXX"));
    // body_json.insert(
    //     String::from("vswitch_ids"),
    //     Value::Array(vec![
    //         Value::from("vsw-2zei30dhflXXXXXXXX"),
    //         Value::from("vsw-2zei30dhflXXXXXXXX"),
    //         Value::from("vsw-2zei30dhflXXXXXXXX"),
    //     ]),
    // );
    // // Query parameters are empty
    // let query_params = &[];
    // // When body is of Json type 
    // let body = RequestBody::Json(body_json);

    // ROA API GET request   API: DeleteCluster  Query linked resources of a specified cluster
    // let method = Method::GET; // Request method
    // let host = "cs.cn-hangzhou.aliyuncs.com"; // Endpoint
    // // Concatenate resource path
    // let uri = format!("/clusters/{}/resources", percent_code("ce196d21571a64be9XXXXXXXX").as_ref()); 
    // let canonical_uri = uri.as_str(); // Resource path, converted to &str type
    // let action = "DescribeClusterResources";   // API name
    // let version = "2015-12-15"; // API version number
    // // Set query parameters
    // let query_params = &[("with_addon_resources", if true { "true" } else { "false" })];  // "true" or "false"
    // // Set body parameter to empty
    // let body = RequestBody:: None;

    // ROA API DELETE request   API: DeleteCluster  DELETE request to delete a pay-as-you-go cluster
    // let method = Method::DELETE;
    // let host = "cs.cn-hangzhou.aliyuncs.com";
    // let uri = format!("/clusters/{}", percent_code("ce0138ff31ad044f8XXXXXXXX").as_ref()); 
    // let canonical_uri = uri.as_str(); // Resource path, converted to &str type
    // let action = "DeleteCluster";
    // let version = "2015-12-15";
    // // Query parameters
    // let query_params = &[];
    // // When body parameter is empty
    // let body = RequestBody:: None;
    
    // SendSms API
    // let method = Method::POST; // Request method
    // let host = "dysmsapi.aliyuncs.com"; // Endpoint
    // let canonical_uri = "/"; // RPC APIs have no resource path, so use a forward slash (/) as the CanonicalURI
    // let action = "SendSms"; // API name
    // let version = "2017-05-25"; // API version number
    // let mut query: Vec<(&str, &str)> = Vec::new();
    // query.push(("PhoneNumbers", "<YOUR_PHONENUMBERS>"));
    // query.push(("TemplateCode", "<YOUR_TEMPLATECODE>"));
    // query.push(("SignName", "<YOUR_SIGNNAME>"));
    // query.push(("TemplateParam", "<YOUR_TEMPLATEPARAM>"));
    // // Query parameters
    // let query_params: &[(&str, &str)] = &query;
    // // When the request body is empty
    // let body = RequestBody:: None;

    // Send the request
    match call_api(
        client.clone(),
        method,                                                  // API request method POST/GET/DELETE                                
        host,                                                    // API endpoint
        canonical_uri,                                           // API resource path
        query_params,                                            // "in":"query" query parameters
        action,                                                  // API name
        version,                                                 // API version number
        body,                                                    // "in":"body" request body parameters, supporting Json/FormData/Binary types
        access_key_id,                                           
        access_key_secret,
    )
    .await {
        Ok(response) => println!("Response: {}", response),
        Err(error) => eprintln!("Exception: {}", error),
    }
}

Shell script example

#!/bin/bash

accessKey_id="<YOUR-ACCESSKEY-ID>"
accessKey_secret="<YOUR-ACCESSKEY-SECRET>"
algorithm="ACS3-HMAC-SHA256"

# Request parameters -- Modify this section as needed
httpMethod="POST"
host="dns.aliyuncs.com"
queryParam=("DomainName=example.com" "RRKeyWord=@")
action="DescribeDomainRecords"
version="2015-01-09"
canonicalURI="/"
# Pass body-type or formdata-type parameters through the body
# body-type parameter: The value of body is a JSON string: "{'key1':'value1','key2':'value2'}", and content-type:application/json; charset=utf-8 must be added to the signature header.
# When the body-type parameter is a binary file: body does not need to be modified, just add content-type:application/octet-stream to the signature header, and add the --data-binary parameter to curl_command.
# formdata-type parameter: The body parameter format is "key1=value1&key2=value2", and content-type:application/x-www-form-urlencoded must be added to the signature header.
body=""

# UTC time in ISO 8601 format
utc_timestamp=$(date +%s)
utc_date=$(date -u -d @${utc_timestamp} +"%Y-%m-%dT%H:%M:%SZ") 
# x-acs-signature-nonce random number
random=$(uuidgen | sed 's/-//g') 

# Signature header
headers="host:${host}
x-acs-action:${action}
x-acs-version:${version}
x-acs-date:${utc_date}
x-acs-signature-nonce:${random}"

# URL encoding function
urlencode() {
    local string="${1}"
    local strlen=${#string}
    local encoded=""
    local pos c o

    for (( pos=0 ; pos<strlen ; pos++ )); do
        c=${string:$pos:1}
        case "$c" in
            [-_.~a-zA-Z0-9] ) o="${c}" ;;
            * )               printf -v o '%%%02X' "'$c"
        esac
        encoded+="${o}"
    done
    echo "${encoded}"
}

# Step 1: Construct the canonical request string
# Flatten all parameters in queryParam
newQueryParam=()

# Traverse each original parameter
for param in "${queryParam[@]}"; do
    # Check if it contains an equal sign to determine if it is a key-value pair
    if [[ "$param" == *"="* ]]; then
        # Split key and value
        IFS='=' read -r key value <<< "$param"

        # URL-encode the value
        value=$(urlencode "$value")

        # Check if the value is a list (by looking for parentheses)
        if [[ "$value" =~ ^\(.+\)$ ]]; then
            # Remove the parentheses
            value="${value:1:-1}"

            # Use IFS to split the value list
            IFS=' ' read -ra values <<< "$value"

            # Add an index for each value
            index=1
            for val in "${values[@]}"; do
                # Remove double quotes
                val="${val%\"}"
                val="${val#\"}"

                # Add to the new array
                newQueryParam+=("$key.$index=$val")
                ((index++))
            done
        else
            # If it is not a list, add it directly
            newQueryParam+=("$key=$value")
        fi
    else
        # If there is no equal sign, keep it as is
        newQueryParam+=("$param")
    fi
done

# Process and sort the new query parameters
sortedParams=()
declare -A paramsMap
for param in "${newQueryParam[@]}"; do
    IFS='=' read -r key value <<< "$param"
    paramsMap["$key"]="$value"
done
# Sort by key
for key in $(echo ${!paramsMap[@]} | tr ' ' '\n' | LC_ALL=C sort); do
    sortedParams+=("$key=${paramsMap[$key]}")
done

# 1.1 Construct the canonical query string
canonicalQueryString=""
first=true
for item in "${sortedParams[@]}"; do
    [ "$first" = true ] && first=false || canonicalQueryString+="&"
    # Check if an equal sign exists
    if [[ "$item" == *=* ]]; then
        canonicalQueryString+="$item"
    else
        canonicalQueryString+="$item="
    fi
done

# 1.2 Process the request body
hashedRequestPayload=$(echo -n "$body" | openssl dgst -sha256 | awk '{print $2}')
headers="${headers}
x-acs-content-sha256:$hashedRequestPayload"

# 1.3 Construct the canonical request headers
canonicalHeaders=$(echo "$headers" | grep -E '^(host|content-type|x-acs-)' | while read line; do
    key=$(echo "$line" | cut -d':' -f1 | tr '[:upper:]' '[:lower:]')
    value=$(echo "$line" | cut -d':' -f2-)
    echo "${key}:${value}"
done | sort | tr '\n' '\n')

signedHeaders=$(echo "$headers" | grep -E '^(host|content-type|x-acs-)' | while read line; do
    key=$(echo "$line" | cut -d':' -f1 | tr '[:upper:]' '[:lower:]')
    echo "$key"
done | sort | tr '\n' ';' | sed 's/;$//')

# 1.4 Construct the canonical request
canonicalRequest="${httpMethod}\n${canonicalURI}\n${canonicalQueryString}\n${canonicalHeaders}\n\n${signedHeaders}\n${hashedRequestPayload}"
echo -e "canonicalRequest=${canonicalRequest}"
echo "+++++++++++++++++++++++++++++++++++++++++++++++++++"

str=$(echo "$canonicalRequest" | sed 's/%/%%/g')
hashedCanonicalRequest=$(printf "${str}" | openssl sha256 -hex | awk '{print $2}')
# Step 2: Construct the string to sign
stringToSign="${algorithm}\n${hashedCanonicalRequest}"
echo -e "stringToSign=$stringToSign"
echo "+++++++++++++++++++++++++++++++++++++++++++++++++++"

# Step 3: Calculate the signature
signature=$(printf "${stringToSign}" | openssl dgst -sha256 -hmac "${accessKey_secret}" | sed 's/^.* //')
echo -e "signature=${signature}"
echo "+++++++++++++++++++++++++++++++++++++++++++++++++++"

# Step 4: Construct the Authorization header
authorization="${algorithm} Credential=${accessKey_id},SignedHeaders=${signedHeaders},Signature=${signature}"
echo -e "authorization=${authorization}"

# Construct the curl command
url="https://$host$canonicalURI"
curl_command="curl -X $httpMethod '$url?$canonicalQueryString'"

# Add request headers
IFS=$'\n'  # Set newline as the new IFS
for header in $headers; do
    curl_command="$curl_command -H '$header'"
done
curl_command+=" -H 'Authorization:$authorization'"
# When the body-type parameter is a binary file, comment out the following line
curl_command+=" -d '$body'"
# When the body-type parameter is a binary file, uncomment the following line
#curl_command+=" --data-binary @"/root/001.png" "

echo "$curl_command"
# Execute the curl command
eval "$curl_command"

C example

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <stdarg.h>
#include <stdint.h>
#include <openssl/hmac.h>
#include <openssl/evp.h>
#include <openssl/sha.h>
#include <openssl/rand.h>
#include <curl/curl.h>

// getenv() gets the Access Key ID and Access Key Secret from environment variables
#define ACCESS_KEY_ID getenv("ALIBABA_CLOUD_ACCESS_KEY_ID")
#define ACCESS_KEY_SECRET getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")
#define ALGORITHM "ACS3-HMAC-SHA256"
#define BUFFER_SIZE 4096

// Struct for sorting
typedef struct {
    char key[256];
    char value[256];
} KeyValuePair;

// Comparison function: sort by key in lexicographical order
int compare_pairs(const void *a, const void *b) {
    return strcmp(((const KeyValuePair *)a)->key, ((const KeyValuePair *)b)->key);
}

// URL encoding
char* percentEncode(const char* str) {
    if (str == NULL) {
        fprintf(stderr, "Input string cannot be null\n");
        return NULL;
    }
    size_t len = strlen(str);
    char* encoded = (char*)malloc(len * 3 + 1);
    if (encoded == NULL) {
        fprintf(stderr, "Memory allocation failed\n");
        free(encoded); 
        return NULL;
    }
    char* ptr = encoded;
    for (size_t i = 0; i < len; i++) {
        unsigned char c = (unsigned char)str[i];
        if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') {
            *ptr++ = c;
        } else {
            ptr += sprintf(ptr, "%%%02X", c);
        }
    }
    *ptr = '\0'; 
    char* finalEncoded = malloc(strlen(encoded) + 1);
    if (finalEncoded) {
        char* fptr = finalEncoded;
        for (size_t j = 0; j < strlen(encoded); j++) {
            if (encoded[j] == '+') {
                strcpy(fptr, "%20");
                fptr += 3; 
            } else if (encoded[j] == '*') {
                strcpy(fptr, "%2A");
                fptr += 3;
            } else if (encoded[j] == '~') {
                *fptr++ = '~';
            } else {
                *fptr++ = encoded[j];
            }
        }
        *fptr = '\0'; 
    }

    free(encoded); 
    return finalEncoded;
}

/**
 * @brief URL-encodes query parameters, sorts them lexicographically, and generates a canonical query string.
 * @param query_params The original query parameter string (e.g., "key1=value1&key2=value2").
 * @return char* The sorted and encoded canonical query string (caller must free the memory).
 */
char* generate_sorted_encoded_query(const char* query_params) {
    if (query_params == NULL || strlen(query_params) == 0) {
        return strdup(""); // Return an empty string for empty parameters
    }

    KeyValuePair pairs[100]; // Supports up to 100 key-value pairs
    int pair_count = 0;

    char* copy = strdup(query_params);
    if (!copy) {
        fprintf(stderr, "Memory allocation failed\n");
        return NULL;
    }

    char* token = NULL;
    char* saveptr = NULL;
    token = strtok_r(copy, "&", &saveptr);

    while (token != NULL && pair_count < 100) {
        char* eq = strchr(token, '=');
        if (eq) {
            size_t key_len = eq - token;
            char key[256], value[256];

            strncpy(key, token, key_len);
            key[key_len] = '\0';

            const char* val = eq + 1;
            strncpy(value, val, sizeof(value) - 1);
            value[sizeof(value) - 1] = '\0';

            char* encoded_key = percentEncode(key);
            char* encoded_value = percentEncode(value);

            strncpy(pairs[pair_count].key, encoded_key, sizeof(pairs[pair_count].key));
            strncpy(pairs[pair_count].value, encoded_value, sizeof(pairs[pair_count].value));
            pair_count++;

            free(encoded_key);
            free(encoded_value);
        }
        token = strtok_r(NULL, "&", &saveptr);
    }

    free(copy);

    // Sort by key
    qsort(pairs, pair_count, sizeof(KeyValuePair), compare_pairs);

    // Concatenate the sorted query string
    char* query_sorted = malloc(BUFFER_SIZE);
    if (!query_sorted) {
        fprintf(stderr, "Memory allocation failed\n");
        return NULL;
    }
    query_sorted[0] = '\0';

    for (int i = 0; i < pair_count; ++i) {
        if (i == 0) {
            snprintf(query_sorted, BUFFER_SIZE, "%s=%s", pairs[i].key, pairs[i].value);
        } else {
            char temp[512];
            snprintf(temp, sizeof(temp), "&%s=%s", pairs[i].key, pairs[i].value);
            strncat(query_sorted, temp, BUFFER_SIZE - strlen(query_sorted) - 1);
        }
    }

    return query_sorted;
}

// HMAC-SHA256 calculation
void hmac256(const char *key, const char *message, char *output) {
    unsigned char hmac[SHA256_DIGEST_LENGTH];
    unsigned int result_len;
    HMAC(EVP_sha256(), key, strlen(key), (unsigned char *)message, strlen(message), hmac, &result_len);
    for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
        sprintf(output + (i * 2), "%02x", hmac[i]);
    }
    output[SHA256_DIGEST_LENGTH * 2] = '\0';
}
// Calculate SHA-256 hash
void sha256_hex(const char *input, char *output) {
    unsigned char hash[SHA256_DIGEST_LENGTH];
    SHA256((unsigned char *)input, strlen(input), hash);
    for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
        sprintf(output + (i * 2), "%02x", hash[i]);
    }
    output[SHA256_DIGEST_LENGTH * 2] = '\0';
}
// Used to generate x-acs-signature-nonce
void generate_uuid(char *uuid, size_t size) {
    if (size < 37) { 
        fprintf(stderr, "Buffer size too small for UUID\n");
        return;
    }
    unsigned char random_bytes[16];
    RAND_bytes(random_bytes, sizeof(random_bytes));
    random_bytes[6] &= 0x0f; // Keep the high 4 bits
    random_bytes[6] |= 0x40; // Set version to 4
    random_bytes[8] &= 0x3f; // Keep the high 2 bits
    random_bytes[8] |= 0x80; // Set variant to 10xx
    snprintf(uuid, size,
             "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x",
             random_bytes[0], random_bytes[1], random_bytes[2], random_bytes[3],
             random_bytes[4], random_bytes[5], random_bytes[6], random_bytes[7],
             random_bytes[8], random_bytes[9], random_bytes[10], random_bytes[11],
             random_bytes[12], random_bytes[13], random_bytes[14], random_bytes[15]);
}
// Upload file
size_t read_file(const char *file_path, char **buffer) {
    FILE *file = fopen(file_path, "rb");
    if (!file) {
        fprintf(stderr, "Cannot open file %s\n", file_path);
        return 0; // Read failed
    }
    fseek(file, 0, SEEK_END);
    size_t file_size = ftell(file);
    fseek(file, 0, SEEK_SET);

    *buffer = (char *)malloc(file_size);
    if (!*buffer) {
        fprintf(stderr, "Failed to allocate memory for file buffer\n");
        fclose(file);
        return 0; // Read failed
    }
    fread(*buffer, 1, file_size, file);
    fclose(file);
    return file_size; // Return the number of bytes read
}
// Calculate Authorization
char* get_authorization(const char *http_method, const char *canonical_uri, const char *host,
                       const char *x_acs_action, const char *x_acs_version, const char *query_params,
                       const char *body, char *authorization_header,
                        char *hashed_payload, char *x_acs_date, char *uuid) {
    // Prepare x-acs-signature-nonce, x-acs-date, x-acs-content-sha256, and the string to be signed
    generate_uuid(uuid, 37);
    // x-acs-date format is yyyy-MM-ddTHH:mm:ssZ, for example, 2025-04-17T07:19:10Z
    time_t now = time(NULL);
    struct tm *utc_time = gmtime(&now);
    strftime(x_acs_date, 64, "%Y-%m-%dT%H:%M:%SZ", utc_time);
    // String to be signed
    char signed_headers[] = "host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version";
    // x-acs-content-sha256 
    sha256_hex(body ? body : "", hashed_payload);
    printf("Generated x-acs-content-sha256: %s\n", hashed_payload);
    // 1. Construct canonical request headers
    char canonical_headers[BUFFER_SIZE];
    snprintf(canonical_headers, sizeof(canonical_headers),
             "host:%s\nx-acs-action:%s\nx-acs-content-sha256:%s\nx-acs-date:%s\nx-acs-signature-nonce:%s\nx-acs-version:%s",
              host, x_acs_action, hashed_payload, x_acs_date, uuid, x_acs_version);
    printf("Canonical Headers:\n%s\n", canonical_headers);

    // 2. Construct the request headers to be signed
    // Sort and encode query parameters
    char* sorted_query_params = generate_sorted_encoded_query(query_params);
    if (!sorted_query_params) {
      fprintf(stderr, "Failed to generate sorted query string\n");
      return NULL;
    }
    char canonical_request[BUFFER_SIZE];
    snprintf(canonical_request, sizeof(canonical_request),
         "%s\n%s\n%s\n%s\n\n%s\n%s",
         http_method,
         canonical_uri,
         sorted_query_params ? sorted_query_params : "",
         canonical_headers,
         signed_headers,
         hashed_payload);
    printf("Canonical Request:\n%s\n", canonical_request);

    // 3. Calculate the SHA-256 hash of the canonical request
    char hashed_canonical_request[SHA256_DIGEST_LENGTH * 2 + 1];
    sha256_hex(canonical_request, hashed_canonical_request);
    printf("hashedCanonicalRequest: %s\n", hashed_canonical_request);
    // 4. Construct the string to sign 
    char string_to_sign[BUFFER_SIZE];
    snprintf(string_to_sign, sizeof(string_to_sign), "%s\n%s", ALGORITHM, hashed_canonical_request);
    printf("stringToSign:\n%s\n", string_to_sign);
    // 5. Calculate the signature 
    char signature[SHA256_DIGEST_LENGTH * 2 + 1];
    hmac256(ACCESS_KEY_SECRET, string_to_sign, signature);
    printf("Signature: %s\n", signature);
    // 6. Construct the Authorization header
    snprintf(authorization_header, BUFFER_SIZE,
             "%s Credential=%s,SignedHeaders=%s,Signature=%s",
             ALGORITHM, ACCESS_KEY_ID, signed_headers, signature);
    printf("Authorization: %s\n", authorization_header);

    return sorted_query_params;
}
// Send the request
void call_api(const char *http_method, const char *canonical_uri, const char *host,
              const char *x_acs_action, const char *x_acs_version, const char *query_params,
              const char *body,const char *content_type, size_t body_length) {
    // Get the parameter values required for signature calculation
    char authorization_header[BUFFER_SIZE];
    char hashed_payload[SHA256_DIGEST_LENGTH * 2 + 1];
    char x_acs_date[64];
    char uuid[37];
    // 1. Initialize curl
    CURL *curl = curl_easy_init();
    if (!curl) {
        fprintf(stderr, "curl_easy_init() failed\n");
        goto cleanup;
    }
    // 2. Calculate the signature (returns sorted and encoded query parameters)
    char *signed_query_params = get_authorization(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, authorization_header, hashed_payload, x_acs_date, uuid);
    // 3. Add request parameters 
    char url[BUFFER_SIZE];
    if (signed_query_params && strlen(signed_query_params) > 0) {
        snprintf(url, sizeof(url), "https://%s%s?%s", host, canonical_uri, signed_query_params);
    } else {
        snprintf(url, sizeof(url), "https://%s%s", host, canonical_uri);
    }
    printf("Request URL: %s\n", url);
    // Free memory
    if (signed_query_params) {
        free(signed_query_params); // Free memory
    }

    // 4. Add headers
    struct curl_slist *headers = NULL;
    char header_value[BUFFER_SIZE];
    snprintf(header_value, sizeof(header_value), "Content-Type: %s", content_type);
    headers = curl_slist_append(headers, header_value);
    snprintf(header_value, sizeof(header_value), "Authorization: %s", authorization_header);
    headers = curl_slist_append(headers, header_value);
    snprintf(header_value, sizeof(header_value), "host: %s", host);
    headers = curl_slist_append(headers, header_value);
    snprintf(header_value, sizeof(header_value), "x-acs-action: %s", x_acs_action);
    headers = curl_slist_append(headers, header_value);
    snprintf(header_value, sizeof(header_value), "x-acs-content-sha256: %s", hashed_payload);
    headers = curl_slist_append(headers, header_value);
    snprintf(header_value, sizeof(header_value), "x-acs-date: %s", x_acs_date);
    headers = curl_slist_append(headers, header_value);
    snprintf(header_value, sizeof(header_value), "x-acs-signature-nonce: %s", uuid);
    headers = curl_slist_append(headers, header_value);
    snprintf(header_value, sizeof(header_value), "x-acs-version: %s", x_acs_version);
    headers = curl_slist_append(headers, header_value);
    curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
    curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, http_method);
    curl_easy_setopt(curl, CURLOPT_URL, url);
    // Other CURL settings: disable SSL verification, add debug information
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
    curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
    // 5. Add body
    if (body) {
        curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, body_length); 
        if (strcmp(content_type, "application/octet-stream") == 0) {
            curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
        } else if (strcmp(content_type, "application/x-www-form-urlencoded") == 0) {
            curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
        } else if (strcmp(content_type, "application/json; charset=utf-8") == 0) {
            curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
        }
    }
    printf("RequestBody:%s\n",body);
    // 6. Send the request
    CURLcode res = curl_easy_perform(curl);
    if (res != CURLE_OK) {
        fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));
        goto cleanup;
    }
cleanup:
    if (headers) curl_slist_free_all(headers);
    if (curl) curl_easy_cleanup(curl);
}
/**
*
     * Signature example. Replace the example parameters in the main method as needed.
     * <p>
     * Get the request method (methods), request parameter name (name), request parameter type (type), and request parameter location (in) from the API metadata.
     * 1. If the request parameter is shown as "in":"query" in the metadata, pass the parameter using query_params. Note: For RPC APIs, this type of parameter can also be passed through the body with content-type as application/x-www-form-urlencoded. See Example 3.
     * 2. If the request parameter is shown as "in": "body" in the metadata, pass the parameter through the body. The MIME type is application/octet-stream or application/json. For RPC APIs, it is not recommended to use application/json. You can use Example 3 instead.
     * 2. If the request parameter is shown as "in": "formData" in the metadata, pass the parameter through the body. The MIME type is application/x-www-form-urlencoded.
*/
int main() {
    // Set response format to UTF-8
    SetConsoleOutputCP(CP_UTF8);
    srand((unsigned int)time(NULL));

    /**
      * RPC API request example: Request parameters are in "query", and the query type is complex.
    */   
    const char *http_method = "POST";
    const char *canonical_uri = "/";
    const char *host = "tds.cn-shanghai.aliyuncs.com";
    const char *x_acs_action = "AddAssetSelectionCriteria";
    const char *x_acs_version = "2018-12-03";

    // Define parameter SelectionKey, string type
    const char *selection_key = "85a561b7-27d5-47ad-a0ec-XXXXXXXX";
    // Define parameter TargetOperationList, a collection of target objects (can be extended)
    struct {
        const char *operation;
        const char *target;
    } targetOperation_list[] = {
        {"add", "i-2ze1j7ocdg9XXXXXXXX"},
        // More entries can be added
        // {"add", "i-abc123xyzXXXXX"},
    };

    int count = sizeof(targetOperation_list) / sizeof(targetOperation_list[0]);
    KeyValuePair pairs[100]; // Store original keys and values
    int pair_count = 0;

    for (int i = 0; i < count; ++i) {
      char op_key[128], target_key[128];
      snprintf(op_key, sizeof(op_key), "TargetOperationList.%d.Operation", i + 1);
      snprintf(target_key, sizeof(target_key), "TargetOperationList.%d.Target", i + 1);

      strncpy(pairs[pair_count].key, op_key, sizeof(pairs[pair_count].key));
      strncpy(pairs[pair_count].value, targetOperation_list[i].operation, sizeof(pairs[pair_count].value));
      pair_count++;

      strncpy(pairs[pair_count].key, target_key, sizeof(pairs[pair_count].key));
      strncpy(pairs[pair_count].value, targetOperation_list[i].target, sizeof(pairs[pair_count].value));
      pair_count++;
}
    // Add SelectionKey parameter
    snprintf(pairs[pair_count].key, sizeof(pairs[pair_count].key), "SelectionKey");
    snprintf(pairs[pair_count].value, sizeof(pairs[pair_count].value), "%s", selection_key);
    pair_count++;

    // Sorting and encoding are done in get_authorization()
    qsort(pairs, pair_count, sizeof(KeyValuePair), compare_pairs);

    // Construct the original query string (unencoded)
    char query_params[BUFFER_SIZE] = {0};
    for (int i = 0; i < pair_count; ++i) {
      if (i == 0) {
        snprintf(query_params, sizeof(query_params), "%s=%s", pairs[i].key, pairs[i].value);
     } else {
        char temp[512];
        snprintf(temp, sizeof(temp), "&%s=%s", pairs[i].key, pairs[i].value);
        strncat(query_params, temp, sizeof(query_params) - strlen(query_params) - 1);
    }
}
    const char *body = ""; // Request body is empty
    const char *content_type = "application/json; charset=utf-8";
    call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));

    /**
      * RPC API request example: Request parameters are in "query"
    */
    // Define API request parameters
    // const char *http_method = "POST";
    // const char *canonical_uri = "/";
    // const char *host = "ecs.cn-hangzhou.aliyuncs.com";
    // const char *x_acs_action = "DescribeInstanceStatus";
    // const char *x_acs_version = "2014-05-26";
    // // Define parameter InstanceId, an array. InstanceId is an optional parameter.
    // const char *instance_ids[] = {
    //     "i-bp11ht4hXXXXXXXX",
    //     "i-bp16maz3XXXXXXXX"
    // };
    // // Concatenate the InstanceId array
    // char InstanceId[BUFFER_SIZE];
    // snprintf(InstanceId, sizeof(InstanceId),
    //          "InstanceId.1=%s&InstanceId.2=%s",
    //         instance_ids[0],
    //         instance_ids[1]);
    // // Define query parameters. Required parameter: RegionId=cn-hangzhou. const char *query_params = "RegionId=cn-hangzhou";
    // char query_params[BUFFER_SIZE];
    // snprintf(query_params, sizeof(query_params),
    //          "%s&RegionId=cn-hangzhou", InstanceId);
    // const char *body = "";
    // const char *content_type = "application/json; charset=utf-8";
    // call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));

      /**
        * RPC API request example: Request parameters are in "body" (file upload scenario)
      */
    // Declare a pointer to store the read file content
    // char *body = NULL;
    // size_t body_length = read_file("<YOUR_FILE_PATH>", &body);
    // if (body_length > 0) {
    //   const char *http_method = "POST";
    //   const char *canonical_uri = "/";
    //   const char *host = "ocr-api.cn-hangzhou.aliyuncs.com";
    //   const char *x_acs_action = "RecognizeGeneral";
    //   const char *x_acs_version = "2021-07-07";
    //   const char *query_params = "";
    //   const char *content_type = "application/octet-stream";
    //   call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, body_length);
    //   free(body);
    // } else {
    //   fprintf(stderr, "File read error\n");
    // }

      /**
       * RPC API request example: Request parameters are in "formData" or "in":"body" (non-file upload scenario)
       */
    // const char *http_method = "POST";
    // const char *canonical_uri = "/";
    // const char *host = "mt.aliyuncs.com";
    // const char *x_acs_action = "TranslateGeneral";
    // const char *x_acs_version = "2018-10-12";
    // char query_params[BUFFER_SIZE];
    // snprintf(query_params, sizeof(query_params), "Context=%s", "Morning");
    // const char *format_type = "text";
    // const char *source_language = "zh";
    // const char *target_language = "en";
    // const char *source_text = "Hello";
    // const char *scene = "general";
    // char body[BUFFER_SIZE];
    // snprintf(body, sizeof(body),
    // "FormatType=%s&SourceLanguage=%s&TargetLanguage=%s&SourceText=%s&Scene=%s",
    // percentEncode(format_type), percentEncode(source_language), percentEncode(target_language),
    // percentEncode(source_text), percentEncode(scene));
    // const char *content_type = "application/x-www-form-urlencoded";
    // printf("formdate_body: %s\n", body);
    // call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));

   // RPC API request example 3: Request parameters are in "formData"
//    const char *http_method = "POST";
//    const char *canonical_uri = "/";
//    const char *host = "sasti.aliyuncs.com";
//    const char *x_acs_action = "AskTextToTextMsg";
//    const char *x_acs_version = "2020-05-12";
//    // query
//    const char *query_params = "";
//    // body
//    const char *Memory = "false";
//    const char *Stream = "true";
//    const char *ProductCode = "sddp_pre";
//    const char *Feature = "{}";
//    const char *Model = "yunsec-llm-latest";
//    const char *Type = "Chat";
//    const char *TopP = "0.9";
//    const char *Temperature = "0.01";
//    const char *Prompt = "Who are you";
//    const char *Application = "sddp_pre";
//    char body[BUFFER_SIZE];
//    snprintf(body, sizeof(body),
//            "Memory=%s&Stream=%s&ProductCode=%s&Feature=%s&Model=%s&Type=%s&TopP=%s&Temperature=%s&Prompt=%s&Application=%s",
//            Memory, Stream, ProductCode, Feature, Model, Type, TopP, Temperature, Prompt, Application);
//    const char *content_type = "application/x-www-form-urlencoded";
//    printf("formdate_body: %s\n", body);
//    call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));

      /**
        * ROA API POST request "in" "body"
      */
//    const char *http_method = "POST";
//    const char *canonical_uri = "/clusters";
//    const char *host = "cs.cn-beijing.aliyuncs.com";
//    const char *x_acs_action = "CreateCluster";
//    const char *x_acs_version = "2015-12-15";
//    const char *query_params = "";
//    char body[BUFFER_SIZE];
//    snprintf(body, sizeof(body),
//             "{\"name\":\"%s\",\"region_id\":\"%s\",\"cluster_type\":\"%s\","
//             "\"vpcid\":\"%s\",\"container_cidr\":\"%s\","
//             "\"service_cidr\":\"%s\",\"security_group_id\":\"%s\","
//             "\"vswitch_ids\":[\"%s\"]}",
//             "Test Cluster", "cn-beijing", "ExternalKubernetes",
//             "vpc-2zeou1uod4yXXXXXXXX", "10.X.X.X/XX",
//             "10.X.X.X/XX", "sg-2ze1a0rlgeXXXXXXXX",
//             "vsw-2zei30dhflXXXXXXXX");
//    const char *content_type = "application/json; charset=utf-8";
//    call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));

      /**
        * ROA API GET request
      */
//    const char *http_method = "GET";
//    char canonical_uri[BUFFER_SIZE];
//    snprintf(canonical_uri, sizeof(canonical_uri), "/clusters/%s/resources", percentEncode("cd1f5ba0dbfa144XXXXXXXX"));
//    const char *host = "cs.cn-beijing.aliyuncs.com";
//    const char *x_acs_action = "DescribeClusterResources";
//    const char *x_acs_version = "2015-12-15";
//    const char *query_params = "with_addon_resources=true";
//    const char *body = "";
//    const char *content_type = "";
//    call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));

      /**
        *  ROA API DELETE request
      */
//    const char *http_method = "DELETE";
//    char canonical_uri[BUFFER_SIZE];
//    snprintf(canonical_uri, sizeof(canonical_uri), "/clusters/%s", percentEncode("cd1f5ba0dbfa144XXXXXXXX"));
//    const char *host = "cs.cn-beijing.aliyuncs.com";
//    const char *x_acs_action = "DeleteCluster";
//    const char *x_acs_version = "2015-12-15";
//    const char *query_params = "";
//    const char *body = "";
//    const char *content_type = "";
//    call_api(http_method, canonical_uri, host, x_acs_action, x_acs_version, query_params, body, content_type, strlen(body));



    // Variables to store generated values
    char authorization_header[BUFFER_SIZE];
    char hashed_payload[SHA256_DIGEST_LENGTH * 2 + 1];
    char x_acs_date[64];
    char uuid[37];
    return 0;
}

FAQ

Why do I receive the "Specified signature does not match our calculation." or "The request signature does not conform to Aliyun standards." error when the signature fails?

Cause:

Based on our experience, most issues occur during the construction of the canonical request. Common causes include the following:

  • Incorrect AccessKey configuration.

  • Incorrect parameter passing location, such as passing query parameters in the request body.

  • The parameters in the canonical query string are not sorted alphabetically.

  • The signed headers are not sorted alphabetically.

  • Spaces are not encoded as %20.

  • An extra URL encoding was performed. During the signature calculation process, URL encoding must be performed only once when you process path parameters and standard query strings. For example, if the error message contains multiple %25 characters, it indicates that the % character was encoded.

Solution:

Note

First, check whether your local code's calculation result is correct based on the Fixed parameter example.

  1. Compare the canonical request in the error message with the canonical request that is calculated locally. If they are different, refer to the common causes listed in this topic and the description in Step 1: Construct a canonical request to carefully check your code.

  2. If the canonical requests are consistent, check whether the string to sign in the error message is different from the string to sign that is calculated locally. If they are different, your hash algorithm may be incorrect.

  3. If the strings to sign are consistent, the cause may be an incorrect AccessKey secret or an incorrect encryption algorithm.

  4. If the issue persists, contact us.

How do I test with Postman?

You cannot directly call OpenAPI using Postman. If you want to test with Postman, perform the following steps:

  1. Calculate the Authorization value using code or a script based on the signature mechanism.

  2. Copy the request header information from the canonical headers to the Headers section in Postman, and enter the Authorization information in the Headers section. The following table provides an example:

    Key

    Example Value

    host

    dysmsapi.aliyuncs.com

    x-acs-action

    SendSms

    x-acs-content-sha256

    e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

    x-acs-date

    2025-04-16T07:45:55Z

    x-acs-signature-nonce

    315484d3-b129-4966-974a-699b7ee56647

    x-acs-version

    2017-05-25

    Authorization

    ACS3-HMAC-SHA256 Credential=testAccessKeyId,SignedHeaders=host;x-acs-action;x-acs-content-sha256;x-acs-date;x-acs-signature-nonce;x-acs-version,Signature=b37aac99faa507472778256374366b7a47ba48adbc484a53ad789db194658a2d

  3. Set parameters in Postman based on the parameter type:

    Note

    The parameter order must be consistent with the order used for signature calculation.

    • For query parameters, enter them in the Params section.

    • For body parameters, enter them in the Body section.

How do I pass request parameters?

In the API metadata, the in field defines the location of each parameter. The location determines how the parameter is passed.

Parameter location

Description

content-type

"in": "query"

Query parameters appear after the question mark (?) at the end of the request URL. Different name=value pairs are separated by an ampersand (&).

Not required. If passed, the value is application/json.

"in": "formData"

Form parameters. The parameters must be concatenated into a string like key1=value1&key2=value2&key3=value3 and passed through the request body. Additionally, if the request parameter type is array or object, you need to convert the value into indexed key-value pairs. For example, an object type value {"key":["value1","value2"]} should be converted to {"key.1":"value1","key.2":"value2"}.

Required. The value is content-type=application/x-www-form-urlencoded.

"in": "body"

Body parameters, passed through the request body.

Required. The value of content-type depends on the request content type. For example:

  • If the request content is JSON data, the content-type is application/json.

  • If the request content is a binary file stream, the content-type is application/octet-stream.

How do you determine the API style if the value of style in the API metadata is not RPC or ROA?

The RPC or ROA style affects the value of CanonicalURI. If the style is a different type, the value of CanonicalURI is determined by the path parameter in the API metadata summary. If the path parameter exists, its value is used as the CanonicalURI. Otherwise, the CanonicalURI is a forward slash (/). For example, in the API metadata for querying ACK cluster lists, the value of path is /api/v1/clusters, so the value of CanonicalURI is /api/v1/clusters.image

How do I pass parameters of the array or object type?

If request parameters are complex data structures, you must convert the parameter values into indexed key-value pairs.

Example 1: The following parameter {"InstanceId":["i-bp10igfmnyttXXXXXXXX","i-bp1incuofvzxXXXXXXXX","i-bp1incuofvzxXXXXXXXX","i-bp10igfmnyttXXXXXXXX","i-bp10igfmnyttXXXXXXXX","i-bp10igfmnyttXXXXXXXX","i-bp10igfmnyttXXXXXXXX","i-bp10igfmnyttXXXXXXXX","i-bp10igfmnyttXXXXXXXX","i-bp10igfmnyttXXXXXXXX","i-bp10igfmnyttXXXXXXXX","i-bp10igfmnyttXXXXXXXX"]} must be converted to:

{
    "InstanceId.1": "i-bp10igfmnyttXXXXXXXX",
    "InstanceId.10": "i-bp10igfmnyttXXXXXXXX",
    "InstanceId.11": "i-bp10igfmnyttXXXXXXXX",
    "InstanceId.12": "i-bp10igfmnyttXXXXXXXX",
    "InstanceId.2": "i-bp1incuofvzxXXXXXXXX",
    "InstanceId.3": "i-bp1incuofvzxXXXXXXXX",
    "InstanceId.4": "i-bp10igfmnyttXXXXXXXX",
    "InstanceId.5": "i-bp10igfmnyttXXXXXXXX",
    "InstanceId.6": "i-bp10igfmnyttXXXXXXXX",
    "InstanceId.7": "i-bp10igfmnyttXXXXXXXX",
    "InstanceId.8": "i-bp10igfmnyttXXXXXXXX",
    "InstanceId.9": "i-bp10igfmnyttXXXXXXXX"
}

Example 2: The following parameter {"ImageId":"win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd","RegionId":"cn-shanghai","Tag":[{"tag1":"value1","tag2":"value2"}]} must be converted to:

{
    "ImageId":"win2019_1809_x64_dtc_zh-cn_40G_alibase_20230811.vhd",
    "RegionId":"cn-shanghai",
    "Tag.1.tag1":"value1",
    "Tag.1.tag2":"value2"
}

How do I get the API version (x-acs-version)?

  1. Go to the Alibaba Cloud OpenAPI Developer Portal and select the cloud product that corresponds to the API you want to call. This example uses ECS.image

  2. On the cloud product's homepage, you can view the recommended API version. For example, the recommended API version for ECS is 2014-05-26.

    image

If I can successfully debug with GET when self-signing, can I use POST?

  • For RPC APIs, both GET and POST requests are supported.

  • For ROA APIs, only one request method is supported.

For more information about how to obtain the request methods that are supported by an API, see OpenAPI metadata.

Why do I receive the "You are not authorized to do this operation." error when calling an API?

Cause: The RAM user that corresponds to the AccessKey you are using does not have the required permissions to call this API.

Solution: For more information, see code 403, You are not authorized to do this operation. Action: xxxx.

How do I get an AccessKey pair?

An AccessKey pair is a permanent access credential that Alibaba Cloud provides to users. It consists of an AccessKey ID and an AccessKey secret. When you call an API to access Alibaba Cloud resources, the system authenticates your identity and verifies the legitimacy of the request based on the AccessKey ID that is carried in the request and the signature that is generated using the AccessKey secret. For more information about how to obtain an AccessKey pair, see Create an AccessKey pair for a RAM user.

Contact us

If you encounter issues that you cannot resolve when you calculate signatures, join the DingTalk group with the ID 147535001692 to contact our on-duty engineers for assistance.

Note

Do not join this group for issues that are unrelated to signature calculation. Otherwise, you may not receive an effective response.