WebHookの署名検証

このセクションでは、CloudGearから送信されてくるWebHookの署名検証に関して説明します。

 

署名検証

WebHookの送信内容は鍵認証方式(RSA-SHA-1)によって担保されます。
アプリケーション開発者は、送信パラメータに含まれる署名を利用して、送信内容の確からしさを検証することができます。

署名に用いる証明書の設定に関しては、「WebHookの送信先を設定する」をご確認ください。

 

実装例

リクエストは、リクエストURL送信パラメータを公開鍵を利用してハッシュ化した値と署名が含まれる送信パラメータ( oauth_signature )を比較することで検証することができます。
以下は、Java Servlet Filterとして実装した場合の例となります。

WebhookFilter
package services.cloudgear.filter;

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
import java.util.Map;
import services.cloudgear.application.SignedVerificationService;

public class WebhookFilter implements Filter {

	@Override
	public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
			throws ServletException, IllegalArgumentException, SecurityException, IOException {

		HttpServletRequest request = (HttpServletRequest)servletRequest;
		Map<String, String[]> requestMap = request.getParameterMap();
		String url = new String(request.getRequestURL());

		// WebHookの検証
		SignedVerificationService service = new SignedVerificationService();
		if(!service.verification(requestMap, url)) {
			// エラー処理
			System.out.println("WebhookFilter#error");
			((HttpServletResponse)servletResponse).sendError(403);
		}

		// WebHookイベントに合わせた処理

		filterChain.doFilter(servletRequest, servletResponse);

	}

	@Override
	public void init(FilterConfig config) throws ServletException {}
	@Override
	public void destroy() {}

}
SignedVerificationService
package services.cloudgear.application;

import java.io.*;
import java.util.*;
import java.net.URLEncoder;
import java.security.*;
import java.security.cert.*;

public class SignedVerificationService {

	// 証明書検証のエントリーポイント
	public boolean verification(Map<String, String[]> requestMap, String url) {
		boolean result = true;
		try {
			// 公開鍵を取得
			File file = new File("webhook.pem");
			InputStream inputStream = new FileInputStream(file);

			// 署名をハッシュにデクリプト
			PublicKey publicKey = getPublicKey(inputStream);
			String[] sig = requestMap.get("oauth_signature");
			// 連結してハッシュ化
			String baseStr = createBaseString(requestMap, url);
			// 検証
			verify(baseStr, sig[0], publicKey);
		} catch(Exception e) {
			result = false;
		}

		return result;
	}

	// 証明書の検証
	private void verify(String signatureBaseString, String signature, PublicKey publicKey) {
		try {
			byte[] signatureBytes = Base64.getDecoder().decode(signature.getBytes("UTF-8"));
			Signature verifier = Signature.getInstance("SHA1withRSA");
			verifier.initVerify(publicKey);
			verifier.update(signatureBaseString.getBytes("UTF-8"));
			if (!verifier.verify(signatureBytes)) {
				// 証明書エラー
			}
		} catch (UnsupportedEncodingException var5) {
			throw new RuntimeException(var5);
		} catch (NoSuchAlgorithmException var6) {
			throw new IllegalStateException(var6);
		} catch (InvalidKeyException var7) {
			throw new IllegalStateException(var7);
		} catch (SignatureException var8) {
			throw new IllegalStateException(var8);
		}
	}

	private PublicKey getPublicKey(InputStream certificate) throws IOException, GeneralSecurityException {
		CertificateFactory fact = CertificateFactory.getInstance("X.509");
		X509Certificate cer = (X509Certificate) fact.generateCertificate(certificate);
		return cer.getPublicKey();
	}
	private String createBaseString(Map<String, String[]> map, String url) throws UnsupportedEncodingException {
		StringBuffer sb = new StringBuffer();
		// method
		sb.append("POST&");
		// URL
		sb.append(URLEncoder.encode(url, "UTF-8") + "&");
		// parameter
		List<String> keyList = new ArrayList<>(map.keySet());
		Collections.sort(keyList);
		StringBuffer params = new StringBuffer();
		for(String key : keyList) {
			if(key.equals("oauth_signature")) continue;
			if(key.indexOf("oauth_") != 0) continue;
			params.append(URLEncoder.encode(key, "UTF-8"));
			params.append("=");
			params.append(URLEncoder.encode((map.get(key))[0], "UTF-8"));
			params.append("&");
		}
		params.deleteCharAt(params.length()-1);
		sb.append(URLEncoder.encode(new String(params), "UTF-8"));
		return new String(sb);
	}
}

署名検証に利用するベースストリング

署名の検証に用いる基本となる文字列は、RFC5849によって定められた方式で作成することになります。
ベースストリングは以下の形式で作成し、各項目はノーマライズされる必要があります。

署名検証のベースストリング
<HTTPメソッド>&<HTTPスキーマ://オーソリティ/パス>&<プロトコルパラメータ>

項目名

解説

HTTPメソッド

署名付きリクエストのHTTPメソッドになります。

HTTPスキーマ

リクエストされたURLのHTTPスキーマになります。

オーソリティ

リクエストされたURLのホストになります。

ポート番号が付加されている場合は、合わせて記述し、Hostヘッダと同じ値にする必要があります。

(80ポート及び443ポートの場合に限って、記述してはいけません)

パス

リクエストされたURLのパスになります。

プロトコルパラメータ

プロトコルによって定められたパラメータになります。
パラメータは「Key=Value」によって示され、各パラメータは「&」によってされます。
また、キーと値は別個にノーマライズ(URIエンコード)されている必要があります。

プロトコルパラメータは、以下になります。

  • oauth_body_hash

  • oauth_consumer_key

  • oauth_nonce

  • oauth_signature_method

  • oauth_timestamp

  • oauth_version
  • クエリーストリング

プロトコルパラメータは、ヘッダ情報に含まれています。
(必ずしもAuthorizationヘッダに含まれているとは限りません。