Skip to content

Commit

Permalink
Merge pull request #5336 from BitGo/aloe/hmac
Browse files Browse the repository at this point in the history
feat: move hmac fns to own package
  • Loading branch information
bitgoAaron authored Jan 7, 2025
2 parents 03402e3 + 589dd29 commit 8c5559e
Show file tree
Hide file tree
Showing 23 changed files with 536 additions and 123 deletions.
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ COPY --from=builder /tmp/bitgo/modules/abstract-lightning /var/modules/abstract-
COPY --from=builder /tmp/bitgo/modules/abstract-utxo /var/modules/abstract-utxo/
COPY --from=builder /tmp/bitgo/modules/blockapis /var/modules/blockapis/
COPY --from=builder /tmp/bitgo/modules/sdk-api /var/modules/sdk-api/
COPY --from=builder /tmp/bitgo/modules/sdk-hmac /var/modules/sdk-hmac/
COPY --from=builder /tmp/bitgo/modules/unspents /var/modules/unspents/
COPY --from=builder /tmp/bitgo/modules/account-lib /var/modules/account-lib/
COPY --from=builder /tmp/bitgo/modules/sdk-coin-algo /var/modules/sdk-coin-algo/
Expand Down Expand Up @@ -126,6 +127,7 @@ cd /var/modules/abstract-lightning && yarn link && \
cd /var/modules/abstract-utxo && yarn link && \
cd /var/modules/blockapis && yarn link && \
cd /var/modules/sdk-api && yarn link && \
cd /var/modules/sdk-hmac && yarn link && \
cd /var/modules/unspents && yarn link && \
cd /var/modules/account-lib && yarn link && \
cd /var/modules/sdk-coin-algo && yarn link && \
Expand Down Expand Up @@ -203,6 +205,7 @@ RUN cd /var/bitgo-express && \
yarn link @bitgo/abstract-utxo && \
yarn link @bitgo/blockapis && \
yarn link @bitgo/sdk-api && \
yarn link @bitgo/sdk-hmac && \
yarn link @bitgo/unspents && \
yarn link @bitgo/account-lib && \
yarn link @bitgo/sdk-coin-algo && \
Expand Down
18 changes: 15 additions & 3 deletions modules/bitgo/test/v2/unit/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ import * as sinon from 'sinon';
import { BitGo } from '../../../src';

describe('Auth', () => {
let sandbox;
beforeEach(() => {
sandbox = sinon.createSandbox();
});
afterEach(() => {
sandbox.restore();
});
describe('Auth V3', () => {
it('should set auth version to 3 when initializing a bitgo object with explicit auth version 3', () => {
const bitgo = new BitGo({ authVersion: 3 });
Expand Down Expand Up @@ -74,7 +81,10 @@ describe('Auth', () => {
const accessToken = `v2x${'0'.repeat(64)}`;
const bitgo = new BitGo({ authVersion: 3, accessToken });

const calculateHMACSpy = sinon.spy(bitgo, 'calculateHMAC');
const crypto = require('crypto');
const createHmacSpy = sinon.spy(crypto, 'createHmac');
const updateSpy = sinon.spy(crypto.Hmac.prototype, 'update');

const verifyResponseStub = sinon.stub(bitgo, 'verifyResponse').returns({
isValid: true,
isInResponseValidityWindow: true,
Expand All @@ -86,8 +96,10 @@ describe('Auth', () => {
const scope = nock(url).get('/').reply(200);

await bitgo.get(url).should.eventually.have.property('status', 200);
calculateHMACSpy.firstCall.calledWith(accessToken, sinon.match('3.0')).should.be.true();
calculateHMACSpy.restore();

createHmacSpy.firstCall.calledWith('sha256', accessToken).should.be.true();
updateSpy.firstCall.calledWith(sinon.match('3.0')).should.be.true();
createHmacSpy.restore();
verifyResponseStub.restore();
scope.done();
});
Expand Down
3 changes: 3 additions & 0 deletions modules/bitgo/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@
{
"path": "../sdk-api"
},
{
"path": "../sdk-hmac"
},
{
"path": "../sdk-coin-ada"
},
Expand Down
1 change: 1 addition & 0 deletions modules/sdk-api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
},
"dependencies": {
"@bitgo/sdk-core": "^28.18.0",
"@bitgo/sdk-hmac": "^1.0.0",
"@bitgo/sjcl": "^1.0.1",
"@bitgo/unspents": "^0.47.17",
"@bitgo/utxo-lib": "^11.2.1",
Expand Down
6 changes: 4 additions & 2 deletions modules/sdk-api/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import querystring from 'querystring';

import { ApiResponseError, BitGoRequest } from '@bitgo/sdk-core';

import { VerifyResponseOptions } from './types';
import { AuthVersion, VerifyResponseOptions } from './types';
import { BitGoAPI } from './bitgoAPI';

const debug = Debug('bitgo:api');
Expand Down Expand Up @@ -178,7 +178,8 @@ export function verifyResponse(
token: string | undefined,
method: VerifyResponseOptions['method'],
req: superagent.SuperAgentRequest,
response: superagent.Response
response: superagent.Response,
authVersion: AuthVersion
): superagent.Response {
// we can't verify the response if we're not authenticated
if (!req.isV2Authenticated || !req.authenticationToken) {
Expand All @@ -193,6 +194,7 @@ export function verifyResponse(
timestamp: response.header.timestamp,
token: req.authenticationToken,
method,
authVersion,
});

if (!verificationResponse.isValid) {
Expand Down
83 changes: 14 additions & 69 deletions modules/sdk-api/src/bitgoAPI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,15 @@ import {
makeRandomKey,
sanitizeLegacyPath,
} from '@bitgo/sdk-core';
import * as sjcl from '@bitgo/sjcl';
import * as sdkHmac from '@bitgo/sdk-hmac';
import * as utxolib from '@bitgo/utxo-lib';
import { bip32, ECPairInterface } from '@bitgo/utxo-lib';
import * as bitcoinMessage from 'bitcoinjs-message';
import { createHmac } from 'crypto';
import { type Agent } from 'http';
import debugLib from 'debug';
import * as _ from 'lodash';
import * as secp256k1 from 'secp256k1';
import * as superagent from 'superagent';
import * as urlLib from 'url';
import {
handleResponseError,
handleResponseResult,
Expand Down Expand Up @@ -396,6 +394,7 @@ export class BitGoAPI implements BitGoBase {
token: this._token,
method,
text: data || '',
authVersion: this._authVersion,
});
req.set('Auth-Timestamp', requestProperties.timestamp.toString());

Expand All @@ -420,7 +419,7 @@ export class BitGoAPI implements BitGoBase {
return onfulfilled(response);
}

const verifiedResponse = verifyResponse(this, this._token, method, req, response);
const verifiedResponse = verifyResponse(this, this._token, method, req, response, this._authVersion);
return onfulfilled(verifiedResponse);
}
: null;
Expand Down Expand Up @@ -455,7 +454,7 @@ export class BitGoAPI implements BitGoBase {
* @returns {*} - the result of the HMAC operation
*/
calculateHMAC(key: string, message: string): string {
return createHmac('sha256', key).update(message).digest('hex');
return sdkHmac.calculateHMAC(key, message);
}

/**
Expand All @@ -467,83 +466,29 @@ export class BitGoAPI implements BitGoBase {
* @param method request method
* @returns {string}
*/
calculateHMACSubject({ urlPath, text, timestamp, statusCode, method }: CalculateHmacSubjectOptions): string {
const urlDetails = urlLib.parse(urlPath);
const queryPath = urlDetails.query && urlDetails.query.length > 0 ? urlDetails.path : urlDetails.pathname;
if (!_.isUndefined(statusCode) && _.isInteger(statusCode) && _.isFinite(statusCode)) {
if (this._authVersion === 3) {
return [method.toUpperCase(), timestamp, queryPath, statusCode, text].join('|');
}
return [timestamp, queryPath, statusCode, text].join('|');
}
if (this._authVersion === 3) {
return [method.toUpperCase(), timestamp, '3.0', queryPath, text].join('|');
}
return [timestamp, queryPath, text].join('|');
calculateHMACSubject(params: CalculateHmacSubjectOptions): string {
return sdkHmac.calculateHMACSubject({ ...params, authVersion: this._authVersion });
}

/**
* Calculate the HMAC for an HTTP request
*/
calculateRequestHMAC({ url: urlPath, text, timestamp, token, method }: CalculateRequestHmacOptions): string {
const signatureSubject = this.calculateHMACSubject({ urlPath, text, timestamp, method });

// calculate the HMAC
return this.calculateHMAC(token, signatureSubject);
calculateRequestHMAC(params: CalculateRequestHmacOptions): string {
return sdkHmac.calculateRequestHMAC({ ...params, authVersion: this._authVersion });
}

/**
* Calculate request headers with HMAC
*/
calculateRequestHeaders({ url, text, token, method }: CalculateRequestHeadersOptions): RequestHeaders {
const timestamp = Date.now();
const hmac = this.calculateRequestHMAC({ url, text, timestamp, token, method });

// calculate the SHA256 hash of the token
const hashDigest = sjcl.hash.sha256.hash(token);
const tokenHash = sjcl.codec.hex.fromBits(hashDigest);
return {
hmac,
timestamp,
tokenHash,
};
calculateRequestHeaders(params: CalculateRequestHeadersOptions): RequestHeaders {
return sdkHmac.calculateRequestHeaders({ ...params, authVersion: this._authVersion });
}

/**
* Verify the HMAC for an HTTP response
*/
verifyResponse({
url: urlPath,
statusCode,
text,
timestamp,
token,
hmac,
method,
}: VerifyResponseOptions): VerifyResponseInfo {
const signatureSubject = this.calculateHMACSubject({
urlPath,
text,
timestamp,
statusCode,
method,
});

// calculate the HMAC
const expectedHmac = this.calculateHMAC(token, signatureSubject);

// determine if the response is still within the validity window (5 minute window)
const now = Date.now();
const isInResponseValidityWindow = timestamp >= now - 1000 * 60 * 5 && timestamp <= now;

// verify the HMAC and timestamp
return {
isValid: expectedHmac === hmac,
expectedHmac,
signatureSubject,
isInResponseValidityWindow,
verificationTime: now,
};
verifyResponse(params: VerifyResponseOptions): VerifyResponseInfo {
return sdkHmac.verifyResponse({ ...params, authVersion: this._authVersion });
}

/**
Expand Down Expand Up @@ -904,7 +849,7 @@ export class BitGoAPI implements BitGoBase {
this._ecdhXprv = responseDetails.ecdhXprv;

// verify the response's authenticity
verifyResponse(this, responseDetails.token, 'post', request, response);
verifyResponse(this, responseDetails.token, 'post', request, response, this._authVersion);

// add the remaining component for easier access
response.body.access_token = this._token;
Expand Down Expand Up @@ -1186,7 +1131,7 @@ export class BitGoAPI implements BitGoBase {
}

// verify the authenticity of the server's response before proceeding any further
verifyResponse(this, this._token, 'post', request, response);
verifyResponse(this, this._token, 'post', request, response, this._authVersion);

const responseDetails = this.handleTokenIssuance(response.body);
response.body.token = responseDetails.token;
Expand Down
59 changes: 10 additions & 49 deletions modules/sdk-api/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
import { EnvironmentName, IRequestTracer, V1Network } from '@bitgo/sdk-core';
import { ECPairInterface } from '@bitgo/utxo-lib';
import { type Agent } from 'http';

export {
supportedRequestMethods,
AuthVersion,
CalculateHmacSubjectOptions,
CalculateRequestHmacOptions,
CalculateRequestHeadersOptions,
RequestHeaders,
VerifyResponseOptions,
VerifyResponseInfo,
} from '@bitgo/sdk-hmac';
export interface BitGoAPIOptions {
accessToken?: string;
authVersion?: 2 | 3;
Expand Down Expand Up @@ -36,54 +45,6 @@ export interface PingOptions {
reqId?: IRequestTracer;
}

export const supportedRequestMethods = ['get', 'post', 'put', 'del', 'patch', 'options'] as const;

export interface CalculateHmacSubjectOptions {
urlPath: string;
text: string;
timestamp: number;
method: (typeof supportedRequestMethods)[number];
statusCode?: number;
}

export interface CalculateRequestHmacOptions {
url: string;
text: string;
timestamp: number;
token: string;
method: (typeof supportedRequestMethods)[number];
}

export interface RequestHeaders {
hmac: string;
timestamp: number;
tokenHash: string;
}

export interface CalculateRequestHeadersOptions {
url: string;
text: string;
token: string;
method: (typeof supportedRequestMethods)[number];
}

export interface VerifyResponseOptions extends CalculateRequestHeadersOptions {
hmac: string;
url: string;
text: string;
timestamp: number;
method: (typeof supportedRequestMethods)[number];
statusCode?: number;
}

export interface VerifyResponseInfo {
isValid: boolean;
expectedHmac: string;
signatureSubject: string;
isInResponseValidityWindow: boolean;
verificationTime: number;
}

export interface AuthenticateOptions {
username: string;
password: string;
Expand Down
3 changes: 3 additions & 0 deletions modules/sdk-api/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
{
"path": "../sdk-core"
},
{
"path": "../sdk-hmac"
},
{
"path": "../utxo-lib"
},
Expand Down
1 change: 1 addition & 0 deletions modules/sdk-hmac/.eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist/
3 changes: 3 additions & 0 deletions modules/sdk-hmac/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
.idea/
dist/
8 changes: 8 additions & 0 deletions modules/sdk-hmac/.mocharc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require: 'ts-node/register'
timeout: '60000'
reporter: 'min'
reporter-option:
- 'cdn=true'
- 'json=false'
exit: true
spec: ['test/**/*.ts']
12 changes: 12 additions & 0 deletions modules/sdk-hmac/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
!dist/
.idea/
.prettierrc.yml
tsconfig.json
src/
test/
scripts/
.nyc_output
CODEOWNERS
node_modules/
.prettierignore
.mocharc.js
2 changes: 2 additions & 0 deletions modules/sdk-hmac/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.nyc_output/
dist/
3 changes: 3 additions & 0 deletions modules/sdk-hmac/.prettierrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
printWidth: 120
singleQuote: true
trailingComma: 'es5'
4 changes: 4 additions & 0 deletions modules/sdk-hmac/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Change Log

All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
Loading

0 comments on commit 8c5559e

Please sign in to comment.