この記事では、メッセージサービス(MNS)からエンドポイントに送信されるリクエストに署名する方法について説明します。エンドポイントに送信されるリクエストの x-mns-signing-cert-url ヘッダーから署名証明書を取得し、リクエストが シンプルメッセージキュー(旧MNS) から送信されたかどうかを確認できます。これにより、悪意のあるリクエストの影響を防ぐことができます。
シンプルメッセージキュー(旧MNS) からエンドポイントへのメッセージプッシュのリクエストでは、シンプルメッセージキュー(旧MNS) は、文字列への署名に RSA-SHA1 アルゴリズムを実装することにより、Authorization ヘッダーに署名を生成します。エンドポイントは、AccessKey ペアを使用して署名を確認できます。次のセクションでは、検証手順について説明します。
手順 1:X509 証明書を取得する
シンプルメッセージキュー(旧MNS) からエンドポイントに送信される HTTP リクエストでは、x-mns-signing-cert-url ヘッダーは、署名証明書の URL を示す文字列を指定します。署名証明書の URL を取得し、証明書から AccessKey ペアを取得するには、文字列で Base64 デコードを実行する必要があります。
https://mnstest.oss-cn-hangzhou.aliyuncs.com/ の場合にのみ、署名証明書は有効です。それ以外の場合、証明書は無効です。詳細については、「署名証明書の URL が Alibaba Cloud URL であるかどうかを確認するにはどうすればよいですか?」をご参照ください。手順 2:署名対象の文字列を計算する
VERB + "\n"
+ CONTENT-MD5 + "\n"
+ CONTENT-TYPE + "\n"
+ DATE + "\n"
+ CanonicalizedMNSHeaders
+ CanonicalizedResource - VERB: HTTP メソッド。
- CONTENT-MD5: リクエストボディの MD5 ハッシュ。
- CONTENT-TYPE: リクエストボディのコンテンツタイプ。このパラメーターの値は小文字にする必要があります。
- DATE: リクエストが送信された時刻。このパラメーターを null に設定することはできません。時刻は UTC 形式である必要があります。
- CanonicalizedMNSHeaders: x-mns- というプレフィックスが付いた HTTP リクエストヘッダー。
- CanonicalizedResource: リクエストの URL。このパラメーターを null に設定することはできません。
例:
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
/notifications 手順 3:Authorization ヘッダーを復号化する
Authorization ヘッダーで Base64 デコードを実行した後、手順 1 で取得した AccessKey ペアを使用して、デコードされた値を復号化できます。
手順 4:署名を確認する
手順 2 で生成された署名対象の文字列と、手順 3 で生成された復号化された文字列を比較します。2 つの文字列が同じであれば、シンプルメッセージキュー(旧MNS) からのリクエストが検証されます。
CanonicalizedHeaders パラメーター(CanonicalizedMNSHeaders というプレフィックスが付いたヘッダー)の次の命名規則に注意する必要があります。
- ヘッダー名は小文字にする必要があります。
- ヘッダーは昇順でソートする必要があります。
- ヘッダー名と値を区切るコロン(:)の前後にスペースを追加しないでください。
- 各ヘッダーの後には改行(\n)が続きます。正規化された MNS ヘッダーx-mns- というプレフィックスが付いたヘッダーが存在しない場合は、 パラメーターを指定しないでください。
追加情報:
- 署名対象の文字列は UTF-8 形式である必要があります。
- RFC 3447 で定義されている sha1WithRSAEncryption 署名アルゴリズムを使用する必要があります。
- テキストのコード変換には Base64 アルゴリズムを使用する必要があります。
Java のサンプルコード
public class SignDemo {
private Boolean authenticate(String method, String uri, Map<String, String> headers) {
try {
// 署名証明書の URL を取得します。
if (!headers.containsKey("x-mns-signing-cert-url")) {
System.out.println("x-mns-signing-cert-url ヘッダーが見つかりません");
return false;
}
String cert = headers.get("x-mns-signing-cert-url");
if (cert.isEmpty()) {
System.out.println("x-mns-signing-cert-url が空です");
return false;
}
cert = new String(Base64.decodeBase64(cert));
System.out.println("x-mns-signing-cert-url:\t" + cert);
// URL を使用して証明書を取得し、証明書から公開鍵を取得します。
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();
// 署名対象の文字列を取得します。
String str2sign = getSignStr(method, uri, headers);
System.out.println("String2Sign:\t" + str2sign);
// Authorization ヘッダーで Base64 デコードを実行します。
String signature = headers.get("Authorization");
byte[] decodedSign = Base64.decodeBase64(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);
}
}