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