Skip to content

Commit a907f82

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

6 files changed

Lines changed: 1302 additions & 52 deletions

File tree

modules/sdk-api/src/api.ts

Lines changed: 73 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -230,46 +230,92 @@ 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+
const result = bitgo.verifyResponse({
266+
hmac,
267+
timestampSec: Number(timestamp),
268+
method: req.v4Method || method,
269+
pathWithQuery: req.v4PathWithQuery || new URL(req.url).pathname + new URL(req.url).search,
270+
bodyHashHex,
271+
authRequestId: authRequestId || req.v4AuthRequestId || '',
272+
statusCode: response.status,
273+
rawToken: req.authenticationToken,
274+
});
243275

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.
276+
verificationResult = result;
277+
responseTimestamp = timestamp;
278+
hmacErrorDetails = { expectedHmac: result.expectedHmac, receivedHmac: hmac, preimage: result.preimage };
279+
} else {
280+
const result = bitgo.verifyResponse({
281+
url: req.url,
282+
hmac: response.header.hmac,
283+
statusCode: response.status,
284+
text: response.text,
285+
timestamp: response.header.timestamp,
286+
token: req.authenticationToken,
287+
method,
288+
authVersion,
289+
});
290+
291+
verificationResult = result;
292+
responseTimestamp = response.header.timestamp;
250293
const partialBitgoToken = token ? token.substring(0, 10) : '';
251-
const errorDetails = {
252-
expectedHmac,
253-
receivedHmac,
254-
hmacInput: signatureSubject,
294+
hmacErrorDetails = {
295+
expectedHmac: result.expectedHmac,
296+
receivedHmac: response.header.hmac,
297+
hmacInput: result.signatureSubject,
255298
requestToken: req.authenticationToken,
256299
bitgoToken: partialBitgoToken,
257300
};
258-
debug('Invalid response HMAC: %O', errorDetails);
259-
throw new ApiResponseError('invalid response HMAC, possible man-in-the-middle-attack', 511, errorDetails);
260301
}
261302

262-
if (bitgo.getAuthVersion() === 3 && !verificationResponse.isInResponseValidityWindow) {
263-
const errorDetails = {
264-
timestamp: response.header.timestamp,
265-
verificationTime: verificationResponse.verificationTime,
266-
};
303+
// --- Common validation for all auth versions ---
304+
if (!verificationResult.isValid) {
305+
debug('Invalid response HMAC: %O', hmacErrorDetails);
306+
throw new ApiResponseError('invalid response HMAC, possible man-in-the-middle-attack', 511, hmacErrorDetails);
307+
}
308+
309+
// v3 and v4 enforce the response validity window; v2 does not
310+
if (authVersion >= 3 && !verificationResult.isInResponseValidityWindow) {
311+
const errorDetails = { timestamp: responseTimestamp, verificationTime: verificationResult.verificationTime };
267312
debug('Server response outside response validity time window: %O', errorDetails);
268313
throw new ApiResponseError(
269314
'server response outside response validity time window, possible man-in-the-middle-attack',
270315
511,
271316
errorDetails
272317
);
273318
}
319+
274320
return response;
275321
}

0 commit comments

Comments
 (0)