Skip to content

Commit 1243948

Browse files
feat(sdk-api): add verification flow for v4
TICKET: CAAS-819
1 parent 81d8ac4 commit 1243948

6 files changed

Lines changed: 1311 additions & 52 deletions

File tree

modules/sdk-api/src/api.ts

Lines changed: 80 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -230,46 +230,99 @@ export function verifyResponse(
230230
return response;
231231
}
232232

233-
const verificationResponse = bitgo.verifyResponse({
234-
url: req.url,
235-
hmac: response.header.hmac,
236-
statusCode: response.status,
237-
text: response.text,
238-
timestamp: response.header.timestamp,
239-
token: req.authenticationToken,
240-
method,
241-
authVersion,
242-
});
233+
// --- Build version-specific params, call bitgo.verifyResponse(), collect error context ---
234+
let verificationResult: {
235+
isValid: boolean;
236+
expectedHmac: string;
237+
isInResponseValidityWindow: boolean;
238+
verificationTime: number;
239+
};
240+
let hmacErrorDetails: Record<string, unknown>;
241+
let responseTimestamp: string | number;
242+
243+
if (authVersion === 4) {
244+
const hmac = response.header['x-signature'];
245+
const timestamp = response.header['x-request-timestamp'];
246+
const authRequestId = response.header['x-auth-request-id'];
247+
248+
if (!hmac || !timestamp) {
249+
// Server didn't sign the response. This can happen legitimately when the
250+
debug(
251+
'v4 response verification skipped: server response (status %d) missing HMAC headers (x-signature: %s, x-request-timestamp: %s)',
252+
response.status,
253+
hmac ? 'present' : 'missing',
254+
timestamp ? 'present' : 'missing'
255+
);
256+
return response;
257+
}
258+
259+
// Hash the raw response body bytes.
260+
// Convert response.text to a Buffer (UTF-8) so we're hashing the actual bytes,
261+
// not relying on Node's implicit string encoding in crypto.update().
262+
const rawResponseBuffer = Buffer.from(response.text || '');
263+
const bodyHashHex = bitgo.calculateBodyHash(rawResponseBuffer);
264+
265+
// req.v4PathWithQuery is always set by requestPatch; fallback parses req.url as a safety net.
266+
let pathWithQuery = req.v4PathWithQuery;
267+
if (!pathWithQuery) {
268+
const parsedUrl = new URL(req.url);
269+
pathWithQuery = parsedUrl.pathname + parsedUrl.search;
270+
}
243271

244-
if (!verificationResponse.isValid) {
245-
// calculate the HMAC
246-
const receivedHmac = response.header.hmac;
247-
const expectedHmac = verificationResponse.expectedHmac;
248-
const signatureSubject = verificationResponse.signatureSubject;
249-
// Log only the first 10 characters of the token to ensure the full token isn't logged.
272+
const result = bitgo.verifyResponse({
273+
hmac,
274+
timestampSec: Number(timestamp),
275+
method: req.v4Method || method,
276+
pathWithQuery,
277+
bodyHashHex,
278+
authRequestId: authRequestId || req.v4AuthRequestId || '',
279+
statusCode: response.status,
280+
rawToken: req.authenticationToken,
281+
});
282+
283+
verificationResult = result;
284+
responseTimestamp = timestamp;
285+
hmacErrorDetails = { expectedHmac: result.expectedHmac, receivedHmac: hmac, preimage: result.preimage };
286+
} else {
287+
const result = bitgo.verifyResponse({
288+
url: req.url,
289+
hmac: response.header.hmac,
290+
statusCode: response.status,
291+
text: response.text,
292+
timestamp: response.header.timestamp,
293+
token: req.authenticationToken,
294+
method,
295+
authVersion,
296+
});
297+
298+
verificationResult = result;
299+
responseTimestamp = response.header.timestamp;
250300
const partialBitgoToken = token ? token.substring(0, 10) : '';
251-
const errorDetails = {
252-
expectedHmac,
253-
receivedHmac,
254-
hmacInput: signatureSubject,
301+
hmacErrorDetails = {
302+
expectedHmac: result.expectedHmac,
303+
receivedHmac: response.header.hmac,
304+
hmacInput: result.signatureSubject,
255305
requestToken: req.authenticationToken,
256306
bitgoToken: partialBitgoToken,
257307
};
258-
debug('Invalid response HMAC: %O', errorDetails);
259-
throw new ApiResponseError('invalid response HMAC, possible man-in-the-middle-attack', 511, errorDetails);
260308
}
261309

262-
if (bitgo.getAuthVersion() === 3 && !verificationResponse.isInResponseValidityWindow) {
263-
const errorDetails = {
264-
timestamp: response.header.timestamp,
265-
verificationTime: verificationResponse.verificationTime,
266-
};
310+
// --- Common validation for all auth versions ---
311+
if (!verificationResult.isValid) {
312+
debug('Invalid response HMAC: %O', hmacErrorDetails);
313+
throw new ApiResponseError('invalid response HMAC, possible man-in-the-middle-attack', 511, hmacErrorDetails);
314+
}
315+
316+
// v3 and v4 enforce the response validity window; v2 does not
317+
if (authVersion >= 3 && !verificationResult.isInResponseValidityWindow) {
318+
const errorDetails = { timestamp: responseTimestamp, verificationTime: verificationResult.verificationTime };
267319
debug('Server response outside response validity time window: %O', errorDetails);
268320
throw new ApiResponseError(
269321
'server response outside response validity time window, possible man-in-the-middle-attack',
270322
511,
271323
errorDetails
272324
);
273325
}
326+
274327
return response;
275328
}

0 commit comments

Comments
 (0)