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のパスになります。 |
プロトコルパラメータ | プロトコルによって定められたパラメータになります。 プロトコルパラメータは、以下になります。
プロトコルパラメータは、ヘッダ情報に含まれています。 |