すべてのプロダクト
Search
ドキュメントセンター

Simple Message Queue (formerly MNS):HTTP プッシュメッセージの署名検証

最終更新日:Apr 23, 2026

Simple Message Queue (旧称:MNS) がトピックメッセージをご利用の HTTP エンドポイントにプッシュすると、リクエストの Authorization ヘッダーに RSA-SHA1 署名が含まれます。この署名を検証することで、メッセージが SMQ から送信されたものであり、改ざんされていないことを確認します。

セキュリティのベストプラクティス

署名検証を実装する前に、以下の要件を確認してください。

  • 証明書 URL の検証。署名証明書の URL は、https://mnstest.oss-cn-hangzhou.aliyuncs.com/ または https://mns-cert.oss-cn-{region}.aliyuncs.com/ で始まる必要があります。異なるプレフィックスを持つ証明書はすべて拒否してください。詳細については、「公開鍵証明書の URL が Alibaba Cloud によって提供されているかどうかの確認方法」をご参照ください。

  • タイムスタンプを確認します。 Date ヘッダーには 15 分間の有効期間があります。 SMQ は、期限切れのリクエストに対してエラーコード 400 を返します。 エンドポイントをリプレイ攻撃から保護するために、15 分以上前のタイムスタンプを持つメッセージを拒否します。 詳細については、「エラーコード」をご参照ください。

署名検証の仕組み

署名検証は 4 つのステップで構成されます。

  1. X.509 署名証明書を取得し、公開鍵を抽出します。

  2. HTTP リクエストから署名文字列を構築します。

  3. Authorization ヘッダーを Base64 デコードし、公開鍵で復号します。

  4. 復号した値と署名文字列を比較します。一致した場合、リクエストが正当であることが確認されます。

受信リクエストの例

SMQ は、ご利用のエンドポイントに、以下のようなヘッダーを持つ HTTP POST リクエストを送信します。

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-06

x-mns-signing-cert-url の値は Base64 エンコードされています。デコードすると、証明書のダウンロード URL が得られます。

変更のお知らせ

重要

2026 年 4 月 7 日より、署名証明書はリージョン固有のデプロイメントにアップグレードされ、近接での証明書ディストリビューションが可能になります。アップグレードの詳細は以下の通りです。

  • 新しい証明書 URL のフォーマット:https://mns-cert.oss-cn-{region}.aliyuncs.com/x509_public_certificate.pem。ここで、{region} は、cn-hangzhou、cn-beijing、cn-shanghai などの Alibaba Cloud のリージョン ID です。

  • 署名アルゴリズム、証明書の公開鍵の内容、署名フォーマットは変更されません。変更されるのは証明書のダウンロード URL のみです。

  • 署名検証コードが証明書 URL のプレフィックスを検証している場合は、古いプレフィックスと新しいプレフィックスの両方をサポートするように検証ロジックをアップグレードしてください。詳細については、「ステップ 1:X.509 証明書の取得」の証明書の有効性チェックの説明をご参照ください。

  • 元の証明書 URL は廃止されます。できるだけ早く対応を完了してください。

ステップ 1:X.509 証明書の取得

受信リクエストから x-mns-signing-cert-url ヘッダーを抽出し、その値を Base64 デコードして証明書 URL を取得します。その URL から証明書をダウンロードし、公開鍵を抽出します。

重要

証明書の有効性チェック

プッシュリクエストを受信したら、偽造されたリクエストを防ぐために、証明書 URL が有効なプレフィックスで始まっていることを確認してください。証明書 URL は、次のいずれかの条件を満たす場合に有効です。

  • URL が https://mnstest.oss-cn-hangzhou.aliyuncs.com/ で始まる (廃止予定。できるだけ早くリージョン化された URL に切り替えてください)。

  • URL が https://mns-cert.oss-cn-{region}.aliyuncs.com/ で始まる。ここで、{region} は、中国 (杭州)、中国 (北京)、中国 (上海)、中国 (深セン) などの Alibaba Cloud のリージョン ID です。

どちらの条件にも一致しない証明書 URL を持つリクエストは拒否してください。詳細については、「公開鍵証明書の URL が Alibaba Cloud によって提供されているかどうかの確認方法」をご参照ください。

ステップ 2:署名文字列の構築

次のフォーマットでリクエストのコンポーネントを連結して、署名文字列を構築します。

StringToSign = HttpMethod + "\n"
             + CONTENT-MD5 + "\n"
             + CONTENT-TYPE + "\n"
             + DATE + "\n"
             + CanonicalizedMNSHeaders
             + CanonicalizedResource;

パラメーター

パラメーター 説明
HttpMethod HTTP メソッドを大文字で指定します (例:POSTGETPUTDELETE)。
CONTENT-MD5 リクエストボディの MD5 ハッシュ。Content-MD5 ヘッダーがない場合は空白のままにします。
CONTENT-TYPE リクエストボディのコンテンツタイプ。Content-Type ヘッダーがない場合は空白のままにします。
DATE Thu, 07 Mar 2012 18:49:58 GMT のような GMT 形式のリクエストタイムスタンプ。 リクエストで x-mns-dateDate の代わりに使用されている場合は、x-mns-date の値を使用します。 このパラメーターは必須です。 SMQ は、リクエストの送信後 15 分以内にリクエストを受信しなければなりません。そうでない場合、SMQ はエラーコード 400 を返します。 詳細については、「エラーコード」をご参照ください。
CanonicalizedMNSHeaders すべての x-mns-* ヘッダーを、小文字の名前でアルファベット順にソートし、key:value\n のフォーマットで連結したもの。詳細については、「CanonicalizedMNSHeaders の構築」をご参照ください。
CanonicalizedResource ドメインとポートを除いた URI パスとクエリ文字列。例えば、http://123.123.XX.XX:8080/api/test?code=200/api/test?code=200 になります。
重要

システムにゲートウェイや他の中継ゲートウェイが設定されている場合は、ゲートウェイによって変更された URI ではなく、HTTP リクエストの元の URI を使用してください。

CanonicalizedMNSHeaders の構築

すべての x-mns-* ヘッダーを小文字の名前でアルファベット順にソートし、連結します。

// リクエストヘッダーを取得
Map<String, String> httpHeaders = request.getHeaders();
// ヘッダーキーをソートし、小文字に変換
sortHeadersKeyAndToLowerCase(httpHeaders);
// x-mns-* ヘッダーを連結
Set<String> keySet = httpHeaders.keySet();
for (String key : keySet) {
    if (key.startsWith("x-mns-")) {
        CanonicalizedMNSHeaders.append(key).append(":")
            .append(httpHeaders.get(key)).append("\n");
    }
}

署名文字列の例

上記のリクエスト例に基づくと、結果の署名文字列は次のようになります。

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 で取得した RSA 公開鍵で復号します。

ステップ 4:比較と検証

ステップ 2 の署名文字列とステップ 3 の復号された値を比較します。

  • 一致:リクエストは正当です。メッセージを処理します。

  • 不一致:リクエストが偽造されている可能性があります。リクエストを拒否します。

Java のサンプルコード

次の例は、4 つのステップすべてを組み合わせたものです。authenticate メソッドは、署名証明書を取得し、署名文字列を構築し、RSA-SHA1 署名を検証します。

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 {
            // ステップ 1:リクエストヘッダーから署名証明書 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;
            }
            // ヘッダー値を Base64 デコードして、実際の証明書 URL を取得
            cert = new String(Base64.decodeBase64(cert));
            System.out.println("x-mns-signing-cert-url:\t" + cert);

            // 証明書をダウンロードして公開鍵を抽出
            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();

            // ステップ 2:署名文字列を構築
            String str2sign = getSignStr(method, uri, headers);
            System.out.println("String2Sign:\t" + str2sign);

            // ステップ 3:Authorization ヘッダーを Base64 デコード
            String signature = headers.get("Authorization");
            byte[] decodedSign = Base64.decodeBase64(signature);

            // ステップ 4:RSA-SHA1 署名を検証
            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("認証結果:" + res);
    }
}