Skip to content

Commit aab2aee

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

File tree

6 files changed

+1296
-52
lines changed

6 files changed

+1296
-52
lines changed

modules/sdk-api/src/api.ts

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -230,46 +230,86 @@ 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 (could be an error before auth middleware)
250+
return response;
251+
}
252+
253+
// Hash the raw response body bytes.
254+
// Convert response.text to a Buffer (UTF-8) so we're hashing the actual bytes,
255+
// not relying on Node's implicit string encoding in crypto.update().
256+
const rawResponseBuffer = Buffer.from(response.text || '');
257+
const bodyHashHex = bitgo.calculateBodyHash(rawResponseBuffer);
258+
259+
const result = bitgo.verifyResponse({
260+
hmac,
261+
timestampSec: Number(timestamp),
262+
method: req.v4Method || method,
263+
pathWithQuery: req.v4PathWithQuery || new URL(req.url).pathname + new URL(req.url).search,
264+
bodyHashHex,
265+
authRequestId: authRequestId || req.v4AuthRequestId || '',
266+
statusCode: response.status,
267+
rawToken: req.authenticationToken,
268+
});
243269

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.
270+
verificationResult = result;
271+
responseTimestamp = timestamp;
272+
hmacErrorDetails = { expectedHmac: result.expectedHmac, receivedHmac: hmac, preimage: result.preimage };
273+
} else {
274+
const result = bitgo.verifyResponse({
275+
url: req.url,
276+
hmac: response.header.hmac,
277+
statusCode: response.status,
278+
text: response.text,
279+
timestamp: response.header.timestamp,
280+
token: req.authenticationToken,
281+
method,
282+
authVersion,
283+
});
284+
285+
verificationResult = result;
286+
responseTimestamp = response.header.timestamp;
250287
const partialBitgoToken = token ? token.substring(0, 10) : '';
251-
const errorDetails = {
252-
expectedHmac,
253-
receivedHmac,
254-
hmacInput: signatureSubject,
288+
hmacErrorDetails = {
289+
expectedHmac: result.expectedHmac,
290+
receivedHmac: response.header.hmac,
291+
hmacInput: result.signatureSubject,
255292
requestToken: req.authenticationToken,
256293
bitgoToken: partialBitgoToken,
257294
};
258-
debug('Invalid response HMAC: %O', errorDetails);
259-
throw new ApiResponseError('invalid response HMAC, possible man-in-the-middle-attack', 511, errorDetails);
260295
}
261296

262-
if (bitgo.getAuthVersion() === 3 && !verificationResponse.isInResponseValidityWindow) {
263-
const errorDetails = {
264-
timestamp: response.header.timestamp,
265-
verificationTime: verificationResponse.verificationTime,
266-
};
297+
// --- Common validation for all auth versions ---
298+
if (!verificationResult.isValid) {
299+
debug('Invalid response HMAC: %O', hmacErrorDetails);
300+
throw new ApiResponseError('invalid response HMAC, possible man-in-the-middle-attack', 511, hmacErrorDetails);
301+
}
302+
303+
// v3 and v4 enforce the response validity window; v2 does not
304+
if (authVersion >= 3 && !verificationResult.isInResponseValidityWindow) {
305+
const errorDetails = { timestamp: responseTimestamp, verificationTime: verificationResult.verificationTime };
267306
debug('Server response outside response validity time window: %O', errorDetails);
268307
throw new ApiResponseError(
269308
'server response outside response validity time window, possible man-in-the-middle-attack',
270309
511,
271310
errorDetails
272311
);
273312
}
313+
274314
return response;
275315
}

0 commit comments

Comments
 (0)