Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// Script Include: HmacUtils
// Purpose: Compute HMAC SHA-256 and constant-time compare.

var HmacUtils = Class.create();
HmacUtils.prototype = {
initialize: function() {},

hmacSha256Hex: function(secret, message) {
var mac = Packages.javax.crypto.Mac.getInstance('HmacSHA256');
var key = new Packages.javax.crypto.spec.SecretKeySpec(
new Packages.java.lang.String(secret).getBytes('UTF-8'),
'HmacSHA256'
);
mac.init(key);
var raw = mac.doFinal(new Packages.java.lang.String(message).getBytes('UTF-8'));

var sb = new Packages.java.lang.StringBuilder();
for (var i = 0; i < raw.length; i++) {
var hex = Packages.java.lang.Integer.toHexString((raw[i] & 0xff) | 0x100).substring(1);
sb.append(hex);
}
return sb.toString();
},

constantTimeEquals: function(a, b) {
var A = String(a || '');
var B = String(b || '');
if (A.length !== B.length) return false;
var diff = 0;
for (var i = 0; i < A.length; i++) diff |= A.charCodeAt(i) ^ B.charCodeAt(i);
return diff === 0;
},

type: 'HmacUtils'
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Webhook receiver with HMAC SHA-256 validation

## What this solves
Inbound webhooks should be verified to ensure the payload really came from the sender. This receiver validates an `X-Signature` header containing an HMAC SHA-256 of the request body using a shared secret. Invalid signatures return HTTP 401.

## Where to use
- Scripted REST API resource script
- Include the `HmacUtils` Script Include in the same app or global

## How it works
- Reads raw request body and the `X-Signature` header
- Computes HMAC SHA-256 using the shared secret
- Compares in constant time to avoid timing attacks
- If valid, inserts the payload into a target table or queues it for processing

## Configure
- Set `SHARED_SECRET` (prefer credentials or encrypted properties)
- Update `TARGET_TABLE` for successful inserts

## References
- Scripted REST APIs
https://www.servicenow.com/docs/bundle/zurich-application-development/page/build/applications/task/create-scripted-rest-api.html
- REST API request/response objects
https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/GlideHTTPRequest/concept/c_scripted-rest-api-request.html
- Java crypto (used server-side)
https://www.servicenow.com/docs/bundle/zurich-api-reference/page/app-store/dev_portal/API_reference/Script/server_apis/concept/java-use.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Scripted REST API Resource Script: Webhook receiver with HMAC validation
(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {
var SHARED_SECRET = gs.getProperty('x_acme.webhook.secret', '');
var TARGET_TABLE = 'x_acme_inbound_webhook'; // replace with your table

try {
var body = request.body && request.body.data ? request.body.data : '';
var signature = request.getHeader('X-Signature') || ''; // hex HMAC hash

if (!SHARED_SECRET) {
response.setStatus(500);
response.setBody({ error: 'Server not configured' });
return;
}
if (!signature || !body) {
response.setStatus(400);
response.setBody({ error: 'Missing signature or body' });
return;
}

var util = new HmacUtils();
var expected = util.hmacSha256Hex(SHARED_SECRET, body);

if (!util.constantTimeEquals(expected, signature)) {
response.setStatus(401);
response.setBody({ error: 'Invalid signature' });
return;
}

// Valid payload: insert a record for processing
var rec = new GlideRecord(TARGET_TABLE);
rec.initialize();
rec.payload = body;
rec.signature = signature;
rec.insert();

response.setStatus(200);
response.setBody({ ok: true });
} catch (e) {
response.setStatus(500);
response.setBody({ error: String(e) });
}
})(request, response);
Loading