When Simple Message Queue (formerly MNS) pushes a topic message to your HTTP endpoint, the request includes an RSA-SHA1 signature in the Authorization header. Verify this signature to confirm that the message originates from SMQ and has not been tampered with.
Security best practices
Review these requirements before you implement signature verification:
Validate the certificate URL. The signing certificate URL must start with
https://mnstest.oss-cn-hangzhou.aliyuncs.com/. Reject any certificate from a different prefix. For details, see How do I check whether the URL of a public key certificate is provided by Alibaba Cloud?Check the timestamp. The
Dateheader has a 15-minute validity window. SMQ returns error code 400 for expired requests. Reject messages with timestamps older than 15 minutes to protect your endpoint from replay attacks. For more information, see Error codes.
How signature verification works
Signature verification consists of four steps:
Retrieve the X.509 signing certificate and extract the public key.
Construct a string-to-sign from the HTTP request.
Base64-decode the
Authorizationheader and decrypt it with the public key.Compare the decrypted value with the string-to-sign. A match confirms the request is authentic.
Sample incoming request
SMQ sends an HTTP POST request to your endpoint with headers similar to the following:
POST /notifications HTTP/1.1
Content-Type: text/xml;charset=utf-8
Content-MD5: ZDgxNjY5ZjFlMDQ5MGM0YWMwMWE5ODlmZDVlYmQxYjI=
Date: Wed, 25 May 2016 10:46:14 GMT
Authorization: Mko2Azg9fhCw8qR6G7AeAFMyzjO9qn7LDA5/t9E+6X5XURXTqBUuhpK+K55UNhrnlE2UdDkRrwDxsaDP5ajQ****
x-mns-request-id: 57458276F0E3D56D7C00****
x-mns-signing-cert-url: aHR0cDovL21uc3Rlc3Qub3NzLWNuLWhhbmd6aG91LmFsaXl1bmNzLmNvbS94NTA5X3B1YmxpY19jZXJ0aWZpY2F0ZS5w****
x-mns-version: 2015-06-06The x-mns-signing-cert-url value is Base64-encoded. After decoding, it yields the certificate download URL.
Step 1: Retrieve the X.509 certificate
Extract the x-mns-signing-cert-url header from the incoming request and Base64-decode its value to get the certificate URL. Download the certificate from that URL and extract the public key.
Only accept certificates from URLs that start with https://mnstest.oss-cn-hangzhou.aliyuncs.com/. Reject requests with any other certificate URL prefix.
Step 2: Construct the string-to-sign
Build the string-to-sign by concatenating request components in this format:
StringToSign = HttpMethod + "\n"
+ CONTENT-MD5 + "\n"
+ CONTENT-TYPE + "\n"
+ DATE + "\n"
+ CanonicalizedMNSHeaders
+ CanonicalizedResource;Parameters
| Parameter | Description |
|---|---|
| HttpMethod | HTTP method in uppercase, such as POST, GET, PUT, or DELETE. |
| CONTENT-MD5 | MD5 hash of the request body. Leave blank if the Content-MD5 header is absent. |
| CONTENT-TYPE | Content type of the request body. Leave blank if the Content-Type header is absent. |
| DATE | Request timestamp in GMT format, such as Thu, 07 Mar 2012 18:49:58 GMT. If the request uses x-mns-date instead of Date, use the x-mns-date value. This parameter is required. SMQ must receive the request within 15 minutes after it is sent. Otherwise, SMQ returns error code 400. For more information, see Error codes. |
| CanonicalizedMNSHeaders | All x-mns-* headers, sorted alphabetically by lowercase name, concatenated in key:value\n format. See Construct CanonicalizedMNSHeaders. |
| CanonicalizedResource | URI path and query string, excluding the domain and port. For example, http://123.123.XX.XX:8080/api/test?code=200 yields /api/test?code=200. |
If gateways or other transit gateways are configured in your system, use the original URI from the HTTP request, not the URI modified by gateways.
Construct CanonicalizedMNSHeaders
Sort all x-mns-* headers alphabetically by lowercase name and concatenate them:
// Get request headers
Map<String, String> httpHeaders = request.getHeaders();
// Sort headers and convert names to lowercase
sortHeadersKeyAndToLowerCase(httpHeaders);
// Concatenate x-mns-* headers
Set<String> keySet = httpHeaders.keySet();
for (String key : keySet) {
if (key.startsWith("x-mns-")) {
CanonicalizedMNSHeaders.append(key).append(":")
.append(httpHeaders.get(key)).append("\n");
}
}Example string-to-sign
Based on the sample request above, the resulting string-to-sign is:
POST
ZDgxNjY5ZjFlMDQ5MGM0YWMwMWE5ODlmZDVlYmQxYjI=
text/xml;charset=utf-8
Wed, 25 May 2016 10:46:14 GMT
x-mns-request-id:57458276F0E3D56D7C00****
x-mns-signing-cert-url:aHR0cDovL21uc3Rlc3Qub3NzLWNuLWhhbmd6aG91LmFsaXl1bmNzLmNvbS94NTA5X3B1YmxpY19jZXJ0aWZpY2F0ZS5w****
x-mns-version:2015-06-06
/notificationsStep 3: Decrypt the Authorization header
Base64-decode the Authorization header value, then decrypt the decoded bytes with the RSA public key from Step 1.
Step 4: Compare and verify
Compare the string-to-sign from Step 2 with the decrypted value from Step 3:
Match: The request is authentic. Process the message.
No match: The request may be forged. Reject it.
Java sample code
The following example combines all four steps. The authenticate method retrieves the signing certificate, constructs the string-to-sign, and verifies the RSA-SHA1 signature.
import org.apache.commons.codec.binary.Base64;
import java.io.DataInputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class SignDemo {
private Boolean authenticate(String method, String uri, Map<String, String> headers) {
try {
// Step 1: Retrieve the signing certificate URL from the request header
if (!headers.containsKey("x-mns-signing-cert-url")) {
System.out.println("x-mns-signing-cert-url Header not found");
return false;
}
String cert = headers.get("x-mns-signing-cert-url");
if (cert.isEmpty()) {
System.out.println("x-mns-signing-cert-url empty");
return false;
}
// Base64-decode the header value to get the actual certificate URL
cert = new String(Base64.decodeBase64(cert));
System.out.println("x-mns-signing-cert-url:\t" + cert);
// Download the certificate and extract the public key
URL url = new URL(cert);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
DataInputStream in = new DataInputStream(conn.getInputStream());
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Certificate c = cf.generateCertificate(in);
PublicKey pk = c.getPublicKey();
// Step 2: Construct the string-to-sign
String str2sign = getSignStr(method, uri, headers);
System.out.println("String2Sign:\t" + str2sign);
// Step 3: Base64-decode the Authorization header
String signature = headers.get("Authorization");
byte[] decodedSign = Base64.decodeBase64(signature);
// Step 4: Verify the RSA-SHA1 signature
java.security.Signature signetcheck = java.security.Signature.getInstance("SHA1withRSA");
signetcheck.initVerify(pk);
signetcheck.update(str2sign.getBytes());
Boolean res = signetcheck.verify(decodedSign);
return res;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
private String getSignStr(String method, String uri, Map<String, String> headers) {
StringBuilder sb = new StringBuilder();
sb.append(method);
sb.append("\n");
sb.append(safeGetHeader(headers, "Content-md5"));
sb.append("\n");
sb.append(safeGetHeader(headers, "Content-Type"));
sb.append("\n");
sb.append(safeGetHeader(headers, "Date"));
sb.append("\n");
List<String> tmp = new ArrayList<String>();
for (Map.Entry<String, String> entry : headers.entrySet()) {
if (entry.getKey().startsWith("x-mns-")) {
tmp.add(entry.getKey() + ":" + entry.getValue());
}
}
Collections.sort(tmp);
for (String kv : tmp) {
sb.append(kv);
sb.append("\n");
}
sb.append(uri);
return sb.toString();
}
private String safeGetHeader(Map<String, String> headers, String name) {
if (headers.containsKey(name)) {
return headers.get(name);
} else {
return "";
}
}
public static void main(String[] args) {
SignDemo sd = new SignDemo();
Map<String, String> headers = new HashMap<String, String>();
headers.put("Authorization", "Mko2Azg9fhCw8qR6G7AeAFMyzjO9qn7LDA5/t9E+6X5XURXTqBUuhpK+K55UNhrnlE2UdDkRrwDxsaDP5ajQ****");
headers.put("Content-md5", "M2ViOTE2ZDEyOTlkODBjMjVkNzM4YjNhNWI3ZWQ1****");
headers.put("Content-Type", "text/xml;charset=utf-8");
headers.put("Date", "Tue, 23 Feb 2016 09:41:06 GMT");
headers.put("x-mns-request-id", "56CC2932F0E3D5BD5306****");
headers.put("x-mns-signing-cert-url", "aHR0cDovL21uc3Rlc3Qub3NzLWNuLWhhbmd6aG91LmFsaXl1bmNzLmNvbS94NTA5X3B1YmxpY19jZXJ0aWZpY2F0ZS5w****");
headers.put("x-mns-version", "2015-06-06");
Boolean res = sd.authenticate("POST", "/notifications", headers);
System.out.println("Authenticate result:" + res);
}
}