Broken Authentication in Restify with Hmac Signatures
Broken Authentication in Restify with Hmac Signatures — how this specific combination creates or exposes the vulnerability
Broken Authentication in a Restify service that relies on Hmac Signatures often stems from implementation choices that weaken the integrity of the signature verification process. When Hmac Signatures are used, the client and server share a secret key; the client creates a signature over selected parts of the request (commonly HTTP method, URL path, selected headers, and a timestamp or nonce), and the server recomputes the signature and compares it to the one provided. If any part of this flow is inconsistent, an attacker can bypass or forge authentication.
Hmac Signatures-Specific Remediation in Restify — concrete code fixes
To harden Hmac Signatures in Restify, ensure strict canonicalization, constant-time comparison, replay protection, and secure handling of the shared secret. Below are concrete, working examples that demonstrate a safer approach.
Example: Secure Hmac Signature verification in Restify
This example shows a Restify server that validates an Hmac-SHA256 signature with timestamp and nonce replay protection, canonical headers, and constant-time comparison. The client builds the signature string from selected headers and the request body, then includes the signature and metadata in headers.
// server.js
const crypto = require('crypto');
const restify = require('restify');
const SHARED_SECRET = process.env.HMAC_SHARED_SECRET; // must be long, random, and stored securely
const REPLAY_CACHE_TTL_MS = 300000; // 5 minutes
const replayCache = new Map(); // in production, use a fast KV with TTL (e.g., Redis)
function constantTimeCompare(a, b) {
if (a.length !== b.length) return false;
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
}
function buildSignatureString(req) {
// Canonicalize: include method, path, selected headers, and body
const timestamp = req.headers['x-timestamp'];
const nonce = req.headers['x-nonce'];
const digest = req.headers['x-content-sha256'];
// Ensure required security headers are present
if (!timestamp || !nonce || !digest) {
throw new Error('Missing required security headers');
}
// Example canonical list; keep this order consistent client and server
const hmacHeaders = ['x-timestamp', 'x-nonce', 'x-content-sha256', 'content-type'];
const headerValues = hmacHeaders.map(h => req.headers[h.toLowerCase()]).join('\n');
return [req.method.toUpperCase(), req.path(), headerValues, digest].join('\n');
}
function verifyRequest(req, res, next) {
try {
const receivedSignature = req.headers['x-signature'];
if (!receivedSignature) {
return next(new restify.UnauthorizedError('Missing signature'));
}
const timestamp = req.headers['x-timestamp'];
const nonce = req.headers['x-nonce'];
// Basic sanity checks on timestamp to prevent replay and time-window abuse
const now = Date.now();
const ts = parseInt(timestamp, 10);
if (isNaN(ts) || Math.abs(now - ts) > 300000) { // 5-minute window
return next(new restify.UnauthorizedError('Timestamp out of bounds'));
}
// Replay protection: track nonce + timestamp for a short window
const cacheKey = `${nonce}:${ts}`;
if (replayCache.has(cacheKey)) {
return next(new restify.UnauthorizedError('Replay detected'));
}
replayCache.set(cacheKey, true);
setTimeout(() => replayCache.delete(cacheKey), REPLAY_CACHE_TTL_MS);
const computed = crypto.createHmac('sha256', SHARED_SECRET)
.update(buildSignatureString(req))
.digest('hex');
if (!constantTimeCompare(computed, receivedSignature)) {
return next(new restify.UnauthorizedError('Invalid signature'));
}
return next();
} catch (err) {
return next(new restify.UnauthorizedError('Invalid request'));
}
}
const server = restify.createServer();
server.pre(verifyRequest);
server.get('/api/resource', (req, res, next) => {
res.send({ message: 'Authenticated and verified' });
return next();
});
server.listen(8080, () => {
console.log('Server listening on port 8080');
});
Corresponding client-side signature creation (Node.js) to ensure alignment:
// client.js
const crypto = require('crypto');
const SHARED_SECRET = process.env.HMAC_SHARED_SECRET;
function buildHeadersAndSignature(method, path, body, headers = {}) {
const timestamp = Date.now().toString();
const nonce = require('crypto').randomBytes(16).toString('hex');
const contentType = headers['content-type'] || 'application/json';
const bodyDigest = crypto.createHash('sha256').update(body || '').digest('hex');
const signatureString = [method.toUpperCase(), path, [headers['x-timestamp'], headers['x-nonce'], 'x-content-sha256', 'content-type'].map(h => headers[h.toLowerCase()]).join('\n'), bodyDigest].join('\n');
const signature = crypto.createHmac('sha256', SHARED_SECRET).update(signatureString).digest('hex');
return {
timestamp,
nonce,
'content-type': contentType,
'x-content-sha256': bodyDigest,
'x-signature': signature
};
}
// Example usage:
const headers = buildHeadersAndSignature('GET', '/api/resource', '', { 'x-timestamp': Date.now().toString(), 'x-nonce': require('crypto').randomBytes(16).toString('hex') });
// Use headers in your HTTP request
Common pitfalls and how they lead to broken authentication with Hmac Signatures
Several specific anti-patterns can weaken Hmac Signatures in Restify and lead to Broken Authentication:
- Inconsistent canonicalization: if the client and server differ in which headers or body parts are included, an attacker can supply a valid signature for a subset while the server expects a superset (or vice versa).
- Missing replay protection: without timestamp and nonce checks, captured requests can be replayed within the time window, leading to authentication bypass or privilege escalation.
- Timing attacks: using non-constant-time comparison leaks information about the signature via response timing, enabling offline guessing.
- Weak or leaked shared secrets: short or predictable secrets make brute-force feasible; storing secrets in code or logs exposes them similarly to credential leaks.
- Ignoring the request body: omitting the body or a body digest from the signature permits attackers to swap the payload while keeping a valid signature.
Related CWEs: authentication
| CWE ID | Name | Severity |
|---|---|---|
| CWE-287 | Improper Authentication | CRITICAL |
| CWE-306 | Missing Authentication for Critical Function | CRITICAL |
| CWE-307 | Brute Force | HIGH |
| CWE-308 | Single-Factor Authentication | MEDIUM |
| CWE-309 | Use of Password System for Primary Authentication | MEDIUM |
| CWE-347 | Improper Verification of Cryptographic Signature | HIGH |
| CWE-384 | Session Fixation | HIGH |
| CWE-521 | Weak Password Requirements | MEDIUM |
| CWE-613 | Insufficient Session Expiration | MEDIUM |
| CWE-640 | Weak Password Recovery | HIGH |