@@ -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