Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading