署名の検証

署名の検証

Webhook署名は、Boxから送信されたWebhookペイロードが送信中に改ざんされていないことを確認するために役立ちます。署名により、中間者攻撃または再生攻撃が成功する可能性を大幅に低減できます。

設定すると、Boxは通知の本文の暗号化ダイジェストを生成し、これをWebhookペイロードのヘッダーに添付します。アプリケーションがペイロードを受信したら、同じダイジェストを計算し、それを受信したダイジェストと比較することにより、署名を検証することをお勧めします。ダイジェストが一致しない場合、ペイロードは信頼できません。

署名キーを頻繁に変更することで、保護レベルをさらに高めることができます。古いキーと新しいキーをスムーズに切り替えられるよう、Boxでは同時に2つの署名キーをサポートしています。

署名設定

アプリケーションの通知に署名を添付するには、まず、アプリケーション用の署名キーを生成する必要があります。

アプリケーションのキーを設定するには、開発者コンソールでアプリケーションに移動します。[Webhook] タブをクリックし、[キーを生成] ボタンを見つけます。

プライマリキーまたはセカンダリキーを生成したら、その値をコピーします。この値は、Webhookペイロードの検証で必要になります。これで、すべてのWebhookにBOX-SIGNATURE-PRIMARYおよびBOX-SIGNATURE-SECONDARYヘッダーペイロードが含まれるようになります。

SDKによる署名の検証

手動で署名を検証することもできますが、Box公式SDKには便利なメソッドが用意されています。

.NET
using Box.V2.Managers;

var body = "{\"type\":\"webhook_event\",\"webhook\":{\"id\":\"1234567890\"},\"trigger\":\"FILE.UPLOADED\",\"source\":{\"id\":\"1234567890\",\"type\":\"file\",\"name\":\"Test.txt\"}}";
var headers = new Dictionary<string, string>()
{
    { "box-delivery-id", "f96bb54b-ee16-4fc5-aa65-8c2d9e5b546f" },
    { "box-delivery-timestamp", "2020-01-01T00:00:00-07:00" },
    { "box-signature-algorithm", "HmacSHA256" } ,
    { "box-signature-primary", "6TfeAW3A1PASkgboxxA5yqHNKOwFyMWuEXny/FPD5hI=" },
    { "box-signature-secondary", "v+1CD1Jdo3muIcbpv5lxxgPglOqMfsNHPV899xWYydo=" },
    { "box-signature-version", "1" }
};
var primaryKey = "Fd28OJrZ8oNxkgmS7TbjXNgrG8v";
var secondaryKey = "KWkROAOiof4zhYUHbAmiVn63cMj"

bool isValid = BoxWebhooksManager.VerifyWebhook(
    deliveryTimestamp: headers["box-delivery-timestamp"],
    signaturePrimary: headers["box-signature-primary"],
    signatureSecondary: headers["box-signature-secondary"],
    payload: body,
    primaryWebhookKey: primaryKey,
    secondaryWebhookKey: secondaryKey
);
Java
// Webhook message contents are shown for demonstration purposes
// Normally these would come from your HTTP handler

// Webhook message HTTP body
String messagePayload = "{"
    + "\"type\":\"webhook_event","
    + "\"webhook\":{"
    +   "\"id\":\"1234567890\""
    + "},"
    + "\"trigger\":\"FILE.UPLOADED\","
    + "\"source\":{"
    +   "\"id\":\"1234567890\","
    +   "\"type\":\"file\","
    +   "\"name\":\"Test.txt\""
    + "}}";

// Webhook message HTTP headers
Map<String, String> messageHeaders = new HashMap<String, String>();
headers.put("BOX-DELIVERY-ID", "f96bb54b-ee16-4fc5-aa65-8c2d9e5b546f");
headers.put("BOX-DELIVERY-TIMESTAMP", "2020-01-01T00:00:00-07:00");
headers.put("BOX-SIGNATURE-ALGORITHM", "HmacSHA256");
headers.put("BOX-SIGNATURE-PRIMARY", "6TfeAW3A1PASkgboxxA5yqHNKOwFyMWuEXny/FPD5hI=");
headers.put("BOX-SIGNATURE-SECONDARY", "v+1CD1Jdo3muIcbpv5lxxgPglOqMfsNHPV899xWYydo=");
headers.put("BOX-SIGNATURE-VERSION", "1");

// Your application's webhook keys, obtained from the Box Developer Console
String primaryKey = "4py2I9eSFb0ezXH5iPeQRcFK1LRLCdip";
String secondaryKey = "Aq5EEEjAu4ssbz8n9UMu7EerI0LKj2TL";

BoxWebHookSignatureVerifier verifier = new BoxWebHookSignatureVerifier(primaryKey, secondaryKey);
boolean isValidMessage = verifier.verify(
    headers.get("BOX-SIGNATURE-VERSION"),
    headers.get("BOX-SIGNATURE-ALGORITHM"),
    headers.get("BOX-SIGNATURE-PRIMARY"),
    headers.get("BOX-SIGNATURE-SECONDARY"),
    messagePayload,
    headers.get("BOX-DELIVERY-TIMESTAMP")
);

if (isValidMessage) {
    // Message is valid, handle it
} else {
    // Message is invalid, reject it
}
Python
body = b'{"webhook":{"id":"1234567890"},"trigger":"FILE.UPLOADED","source":{"id":"1234567890","type":"file","name":"Test.txt"}}'
headers = {
    'box-delivery-id': 'f96bb54b-ee16-4fc5-aa65-8c2d9e5b546f',
    'box-delivery-timestamp': '2020-01-01T00:00:00-07:00',
    'box-signature-algorithm': 'HmacSHA256',
    'box-signature-primary': '4KvFa5/unRL8aaqOlnbInTwkOmieZkn1ZVzsAJuRipE=',
    'box-signature-secondary': 'yxxwBNk7tFyQSy95/VNKAf1o+j8WMPJuo/KcFc7OS0Q=',
    'box-signature-version': '1',
}
is_validated = Webhook.validate_message(body, headers, primary_key, secondary_key)
print('The webhook message is validated to: {0}'.format(is_validated))
Node
const BoxSDK = require('box-node-sdk');
let body = '{"type":"webhook_event","webhook":{"id":"1234567890"},"trigger":"FILE.UPLOADED","source":{"id":"1234567890","type":"file","name":"Test.txt"}}',
	headers = {
		'box-delivery-id': 'f96bb54b-ee16-4fc5-aa65-8c2d9e5b546f',
		'box-delivery-timestamp': '2020-01-01T00:00:00-07:00',
		'box-signature-algorithm': 'HmacSHA256',
		'box-signature-primary': '6TfeAW3A1PASkgboxxA5yqHNKOwFyMWuEXny/FPD5hI=',
		'box-signature-secondary': 'v+1CD1Jdo3muIcbpv5lxxgPglOqMfsNHPV899xWYydo=',
		'box-signature-version': '1'
	},
	primaryKey = 'SamplePrimaryKey',
	secondaryKey = 'SampleSecondaryKey';

let isValid = BoxSDK.validateWebhookMessage(body, headers, primaryKey, secondaryKey);
if (isValid) {
	// message is valid, accept
} else {
	// message is NOT valid, reject
}

手動による署名の検証

署名の検証は基本的に以下の手順で行います。

タイムスタンプの検証

ペイロードのBOX-DELIVERY-TIMESTAMPヘッダーのタイムスタンプが10分以内のものであることを確認します。

Node
var timestamp = headers['BOX-DELIVERY-TIMESTAMP'];
var date = Date.parse(timestamp);
var expired = Date.now() - date > 10*60*1000;
Python
import dateutil.parser
import pytz
import datetime

timestamp = headers["BOX-DELIVERY-TIMESTAMP"]
date = dateutil.parser.parse(timestamp).astimezone(pytz.utc)

now = datetime.datetime.now(pytz.utc)
delta = datetime.timedelta(minutes=10)
expiry_date = now - deltaMinutes

expired = date >= expiry_date

HMAC署名の計算

開発者コンソールでアプリケーションに設定されている2つの署名のいずれかを使用して、ペイロードのHMACを計算します。

最初にペイロードの本文のバイトを追加してから、BOX-DELIVERY-TIMESTAMPヘッダーにあるタイムスタンプのバイトを追加してください。

Node
var crypto = require('crypto');

var primaryKey = '...';
var secondaryKey = '...';

var payload = '{"type":"webhook_event"...}';

var hmac1 = crypto.createHmac('sha256', primaryKey);
hmac1.update(payload);
hmac1.update(timestamp);

var hmac2 = crypto.createHmac('sha256', secondaryKey);
hmac2.update(payload);
hmac2.update(timestamp);
Python
import hmac
import hashlib

primary_key = '...'
secondary_key = '...'

payload = "{\"type\":\"webhook_event\"...}"

bytes = bytes(payload, 'utf-8') + bytes(timestamp, 'utf-8')

hmac1 = hmac.new(primary_key, bytes, hashlib.sha256).digest()
hmac2 = hmac.new(secondary_key, bytes, hashlib.sha256).digest()

Base64変換

HMACをBase64でエンコードされたダイジェストに変換します。

Node
var digest1 = hmac1.digest('base64');
var digest2 = hmac2.digest('base64');
Python
import base64

digest1 = base64.b64encode(hmac1)
digest2 = base64.b64encode(hmac2)

署名の比較

エンコードされたダイジェストをBOX-SIGNATURE-PRIMARYまたはBOX-SIGNATURE-SECONDARYヘッダーの値と比較します。

BOX-SIGNATURE-PRIMARYヘッダーの値はプライマリキーで作成されたダイジェストと比較し、BOX-SIGNATURE-SECONDARYヘッダーの値はセカンダリキーで作成されたダイジェストと比較してください。

Node
var signature1 = headers['BOX-SIGNATURE-SECONDARY'];
var signature2 = headers['BOX-SIGNATURE-PRIMARY'];

var primarySignatureValid = digest1 === signature1
var secondarySignatureValid = digest2 === signature2

var valid = !expired && (primarySignatureValid || secondarySignatureValid)
Python
signature1 = headers["BOX-SIGNATURE-SECONDARY"]
signature2 = headers["BOX-SIGNATURE-PRIMARY"]

primary_sig_valid = digest1 === signature1
secondary_sig_valid = digest2 === signature2

valid = !expired && (primary_sig_valid || secondary_sig_valid)

HTTPヘッダー名では大文字と小文字が区別されないため、クライアントでは、すべてのヘッダーの名前を標準化された小文字または大文字の形式に変換してから、ヘッダーの値を確認するのが理想的です。

署名のローテーション

有効にした場合、Boxは常にすべてのWebhookペイロードで2つの署名を送信します。少なくとも1つの署名が有効であれば、アプリケーションはペイロードを信頼できます。一度に1つの署名キーを更新すると、アプリケーションは常に少なくとも1つの有効な署名を含むペイロードを受信することになります。

循環の手順

以下の手順は、開発者コンソールでプライマリキーとセカンダリキーを作成済みで、どちらかのキーを置き換える準備ができていることを前提としています。

これらの手順に従うことにより、競合することなく、2つの新しいキーを使ってアプリケーションを構成できます。

  1. 開発者コンソールで [リセット] ボタンをクリックして、プライマリキーを変更します。
  2. 新しいプライマリキーでアプリケーションを更新します。アプリケーションは、引き続き古いプライマリキーを含む通知を受信する可能性がありますが、セカンダリキーがまだ有効であるため、Webhookは今後も正しく処理されます。
  3. 古いプライマリキーを持つWebhookが動作していないことを確認できたら、同じプロセスを使用してセカンダリキーを更新できます。