-
Notifications
You must be signed in to change notification settings - Fork 302
feat(sdk-hmac): add v4 canonical preimage construction #8079
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
MohammedRyaan786
wants to merge
1
commit into
master
Choose a base branch
from
CAAS-660-canonicalization-helper
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,144
−2
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,335 @@ | ||
| /** | ||
| * @prettier | ||
| * | ||
| * V4 HMAC Authentication Module | ||
| * | ||
| * This module implements the v4 authentication scheme which uses a canonical | ||
| * preimage construction with newline-separated fields and body hashing. | ||
| * | ||
| * Key differences from v2/v3: | ||
| * - Separator: newline (\n) instead of pipe (|) | ||
| * - Body: SHA256 hash of raw bytes instead of actual body content | ||
| * - Timestamp: seconds instead of milliseconds | ||
| * - New field: authRequestId for request tracking | ||
| * - Trailing newline in preimage | ||
| * - Support for x-original-* headers (proxy scenarios) | ||
| */ | ||
|
|
||
| import { timingSafeEqual } from 'crypto'; | ||
| import { | ||
| hmacSha256, | ||
| sha256Hex, | ||
| normalizeMethod, | ||
| getTimestampSec, | ||
| extractPathWithQuery, | ||
| type HashableData, | ||
| } from './util'; | ||
| import { | ||
| CalculateV4PreimageOptions, | ||
| CalculateV4RequestHmacOptions, | ||
| CalculateV4RequestHeadersOptions, | ||
| V4RequestHeaders, | ||
| VerifyV4ResponseOptions, | ||
| VerifyV4ResponseInfo, | ||
| } from './types'; | ||
|
|
||
| /** | ||
| * Build canonical preimage for v4 authentication. | ||
| * | ||
| * The preimage is constructed as newline-separated values with a trailing newline: | ||
| * ``` | ||
| * {timestampSec} | ||
| * {METHOD} | ||
| * {pathWithQuery} | ||
| * {bodyHashHex} | ||
| * {authRequestId} | ||
| * ``` | ||
| * | ||
| * This function normalizes the HTTP method to uppercase and handles the | ||
| * legacy 'del' method conversion to 'DELETE'. | ||
| * | ||
| * @param options - The preimage components | ||
| * @returns Newline-separated canonical preimage string with trailing newline | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const preimage = calculateV4Preimage({ | ||
| * timestampSec: 1761100000, | ||
| * method: 'post', | ||
| * pathWithQuery: '/v2/wallets/transfer?foo=bar', | ||
| * bodyHashHex: '0d5e3b7a8f9c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e', | ||
| * authRequestId: '1b7a1d2b-7a2f-4e4b-a1f8-c2a5a0f84e3e', | ||
| * }); | ||
| * | ||
| * // Result: | ||
| * // "1761100000\nPOST\n/v2/wallets/transfer?foo=bar\n0d5e3b...d6e\n1b7a1d2b-7a2f-4e4b-a1f8-c2a5a0f84e3e\n" | ||
| * ``` | ||
| */ | ||
| export function calculateV4Preimage({ | ||
| timestampSec, | ||
| method, | ||
| pathWithQuery, | ||
| bodyHashHex, | ||
| authRequestId, | ||
| }: CalculateV4PreimageOptions): string { | ||
| const normalizedMethod = normalizeMethod(method); | ||
|
|
||
| // Build newline-separated preimage with trailing newline | ||
| const components = [timestampSec.toString(), normalizedMethod, pathWithQuery, bodyHashHex, authRequestId]; | ||
|
|
||
| return components.join('\n') + '\n'; | ||
| } | ||
|
|
||
| /** | ||
| * Calculate SHA256 hash of body and return as lowercase hex string. | ||
| * | ||
| * This is used to compute the bodyHashHex field for v4 authentication. | ||
| * The hash is computed over the raw bytes of the request body, ensuring | ||
| * that the exact bytes sent over the wire are used for signature calculation. | ||
| * | ||
| * Accepts common byte representations for Node.js and browser environments, | ||
| * including Uint8Array and ArrayBuffer for Fetch API compatibility. | ||
| * | ||
| * @param body - Raw request body (string, Buffer, Uint8Array, or ArrayBuffer) | ||
| * @returns Lowercase hex SHA256 hash (64 characters) | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Node.js with Buffer | ||
| * const hash1 = calculateBodyHash(Buffer.from('{"address":"tb1q...","amount":100000}')); | ||
| * | ||
| * // Browser with Uint8Array | ||
| * const hash2 = calculateBodyHash(new TextEncoder().encode('{"address":"tb1q..."}')); | ||
| * | ||
| * // Browser with ArrayBuffer | ||
| * const hash3 = calculateBodyHash(await response.arrayBuffer()); | ||
| * | ||
| * // All return: '0d5e3b7a8f...' (64 character hex string) | ||
| * ``` | ||
| */ | ||
| export function calculateBodyHash(body: HashableData): string { | ||
| return sha256Hex(body); | ||
| } | ||
|
|
||
| /** | ||
| * Calculate the HMAC-SHA256 signature for a v4 HTTP request. | ||
| * | ||
| * This function: | ||
| * 1. Builds the canonical preimage from the provided options | ||
| * 2. Computes HMAC-SHA256 of the preimage using the raw access token | ||
| * | ||
| * @param options - Request parameters and raw access token | ||
| * @returns Lowercase hex HMAC-SHA256 signature | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const hmac = calculateV4RequestHmac({ | ||
| * timestampSec: 1761100000, | ||
| * method: 'POST', | ||
| * pathWithQuery: '/v2/wallets/transfer', | ||
| * bodyHashHex: '0d5e3b...', | ||
| * authRequestId: '1b7a1d2b-7a2f-4e4b-a1f8-c2a5a0f84e3e', | ||
| * rawToken: 'your-raw-token', | ||
| * }); | ||
| * ``` | ||
| */ | ||
| export function calculateV4RequestHmac({ | ||
| timestampSec, | ||
| method, | ||
| pathWithQuery, | ||
| bodyHashHex, | ||
| authRequestId, | ||
| rawToken, | ||
| }: CalculateV4RequestHmacOptions): string { | ||
| const preimage = calculateV4Preimage({ | ||
| timestampSec, | ||
| method, | ||
| pathWithQuery, | ||
| bodyHashHex, | ||
| authRequestId, | ||
| }); | ||
|
|
||
| return hmacSha256(rawToken, preimage); | ||
| } | ||
|
|
||
| /** | ||
| * Generate all headers required for v4 authenticated requests. | ||
| * | ||
| * This is a convenience function that: | ||
| * 1. Generates the current timestamp (in seconds) | ||
| * 2. Calculates the body hash from raw bytes | ||
| * 3. Computes the HMAC signature | ||
| * 4. Returns all values needed for request headers | ||
| * | ||
| * @param options - Request parameters including raw body and raw token | ||
| * @returns Object containing all v4 authentication header values | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * const headers = calculateV4RequestHeaders({ | ||
| * method: 'POST', | ||
| * pathWithQuery: '/v2/wallets/transfer?foo=bar', | ||
| * rawBody: Buffer.from('{"address":"tb1q..."}'), | ||
| * rawToken: 'your-token-key', | ||
| * authRequestId: '1b7a1d2b-7a2f-4e4b-a1f8-c2a5a0f84e3e', | ||
| * }); | ||
| * | ||
| * // Use headers to set: | ||
| * // - Auth-Timestamp: headers.timestampSec | ||
| * // - HMAC: headers.hmac | ||
| * // - X-Body-Hash: headers.bodyHashHex | ||
| * // - X-Request-Id: headers.authRequestId | ||
| * ``` | ||
| */ | ||
| export function calculateV4RequestHeaders({ | ||
| method, | ||
| pathWithQuery, | ||
| rawBody, | ||
| rawToken, | ||
| authRequestId, | ||
| }: CalculateV4RequestHeadersOptions): V4RequestHeaders { | ||
| const timestampSec = getTimestampSec(); | ||
| const bodyHashHex = calculateBodyHash(rawBody); | ||
|
|
||
| const hmac = calculateV4RequestHmac({ | ||
| timestampSec, | ||
| method, | ||
| pathWithQuery, | ||
| bodyHashHex, | ||
| authRequestId, | ||
| rawToken, | ||
| }); | ||
|
|
||
| return { | ||
| hmac, | ||
| timestampSec, | ||
| bodyHashHex, | ||
| authRequestId, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Build canonical preimage for v4 response verification. | ||
| * | ||
| * Response preimage includes the status code and uses the same format: | ||
| * ``` | ||
| * {timestampSec} | ||
| * {METHOD} | ||
| * {pathWithQuery} | ||
| * {statusCode} | ||
| * {bodyHashHex} | ||
| * {authRequestId} | ||
| * ``` | ||
| * | ||
| * @param options - Response verification parameters | ||
| * @returns Newline-separated canonical preimage string with trailing newline | ||
| */ | ||
| export function calculateV4ResponsePreimage({ | ||
| timestampSec, | ||
| method, | ||
| pathWithQuery, | ||
| statusCode, | ||
| bodyHashHex, | ||
| authRequestId, | ||
| }: Omit<VerifyV4ResponseOptions, 'hmac' | 'rawToken'>): string { | ||
| const normalizedMethod = normalizeMethod(method); | ||
|
|
||
| const components = [ | ||
| timestampSec.toString(), | ||
| normalizedMethod, | ||
| pathWithQuery, | ||
| statusCode.toString(), | ||
| bodyHashHex, | ||
| authRequestId, | ||
| ]; | ||
|
|
||
| return components.join('\n') + '\n'; | ||
| } | ||
|
|
||
| /** | ||
| * Verify the HMAC signature of a v4 HTTP response. | ||
| * | ||
| * This function: | ||
| * 1. Reconstructs the canonical preimage from response data | ||
| * 2. Calculates the expected HMAC | ||
| * 3. Compares with the received HMAC | ||
| * 4. Checks if the timestamp is within the validity window | ||
| * | ||
| * The validity window is: | ||
| * - 5 minutes backwards (to account for clock skew and network latency) | ||
| * - 1 minute forwards (to account for minor clock differences) | ||
| * | ||
| * @param options - Response data and raw token for verification | ||
| * @returns Verification result including validity and diagnostic info | ||
| */ | ||
| export function verifyV4Response({ | ||
| hmac, | ||
| timestampSec, | ||
| method, | ||
| pathWithQuery, | ||
| bodyHashHex, | ||
| authRequestId, | ||
| statusCode, | ||
| rawToken, | ||
| }: VerifyV4ResponseOptions): VerifyV4ResponseInfo { | ||
| // Build the response preimage | ||
| const preimage = calculateV4ResponsePreimage({ | ||
| timestampSec, | ||
| method, | ||
| pathWithQuery, | ||
| statusCode, | ||
| bodyHashHex, | ||
| authRequestId, | ||
| }); | ||
|
|
||
| // Calculate expected HMAC | ||
| const expectedHmac = hmacSha256(rawToken, preimage); | ||
|
|
||
| // Use constant-time comparison to prevent timing side-channel attacks | ||
| const hmacBuffer = Buffer.from(hmac, 'hex'); | ||
| const expectedHmacBuffer = Buffer.from(expectedHmac, 'hex'); | ||
| const isHmacValid = | ||
| hmacBuffer.length === expectedHmacBuffer.length && timingSafeEqual(hmacBuffer, expectedHmacBuffer); | ||
|
|
||
MohammedRyaan786 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // Check timestamp validity window | ||
| const nowSec = getTimestampSec(); | ||
| const backwardValidityWindowSec = 5 * 60; // 5 minutes | ||
| const forwardValidityWindowSec = 1 * 60; // 1 minute | ||
| const isInResponseValidityWindow = | ||
| timestampSec >= nowSec - backwardValidityWindowSec && timestampSec <= nowSec + forwardValidityWindowSec; | ||
|
|
||
| return { | ||
| isValid: isHmacValid, | ||
| expectedHmac, | ||
MohammedRyaan786 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| preimage, | ||
| isInResponseValidityWindow, | ||
| verificationTime: Date.now(), | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Extract path with query from x-original-uri header or request URL. | ||
| * Always canonicalizes to pathname + search to handle absolute URLs. | ||
| * | ||
| * @param xOriginalUri - Value of x-original-uri header (if present) | ||
| * @param requestUrl - The actual request URL | ||
| * @returns The canonical path with query to use for preimage calculation | ||
| * | ||
| */ | ||
| export function getPathWithQuery(xOriginalUri: string | undefined, requestUrl: string): string { | ||
| // Prefer x-original-uri if available (proxy scenario) | ||
| const rawPath = xOriginalUri ?? requestUrl; | ||
| return extractPathWithQuery(rawPath); | ||
| } | ||
MohammedRyaan786 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| /** | ||
| * Get method from x-original-method header or actual request method. | ||
| * | ||
| * @param xOriginalMethod - Value of x-original-method header (if present) | ||
| * @param requestMethod - The actual request method | ||
| * @returns The method to use for preimage calculation | ||
| */ | ||
| export function getMethod(xOriginalMethod: string | undefined, requestMethod: string): string { | ||
| // Prefer x-original-method if available (proxy scenario) | ||
| return xOriginalMethod ?? requestMethod; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,4 @@ | ||
| export * from './hmac'; | ||
| export * from './hmacv4'; | ||
| export * from './util'; | ||
| export * from './types'; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.