Request Signing and Verification

Overview

Your integration must be able to verify the authenticity of requests coming from BitGo. To do this, BitGo recommends that you use JSON Web Token (JWT) with the RS256 hashing algorithm, fed by 2048-bit RSA.

BitGo generates and manages a Rivest–Shamir–Adleman (RSA) key pair and provides you with the public key during signature verification.

To prevent replay attacks, BitGo recommends using JWT ID and a finite expiry of 1 minute or less. The uniqueness of the JWT ID should be enforced in your system when you receive requests. For convenience, the JWT ID can be a time-based string.

BitGo sends the generated JWT to all APIs that you provision in the request headers. The signature is contained in the X-BitGo-Signature header.

To learn more about JWT, view the documentation and GitHub repo.

Prerequisites

  • Get Started - Ensure your access token has the following permissions:
    • Settlement Network - Read
    • Settlement Network - Write
  • Fund Go Account
  • Sign a BitGo Network license. To learn more, contact sales@bitgo.com.

Steps

The following code sample generates these keys

  • openssl genrsa -out private.test.pem 2048
  • openssl rsa -in private.test.pem -outform PEM -pubout -out public.test.pem
  • JavaScript
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 import jwt from 'jsonwebtoken'; import fs from 'fs' import path from 'path'; import { expect } from 'chai'; import { v4 as uuid } from 'uuid'; describe('JWT auth methodology', () => { const privateKey = fs.readFileSync(path.resolve(__dirname, 'private.test.pem')); const publicKey = fs.readFileSync(path.resolve(__dirname, 'public.test.pem')); it('should create and verify a signed jwt', () => { const token = jwt.sign( { foo: 'bar' }, { key: privateKey, passphrase: '' }, { algorithm: 'RS256', jwtid: `${(new Date()).toISOString()}:${uuid()}`, expiresIn: 100 }, ); jwt.verify(token, publicKey); }); it('should fail to verify an expired jwt', () => { const token = jwt.sign( { foo: 'bar' }, { key: privateKey, passphrase: '' }, { algorithm: 'RS256', jwtid: `${(new Date()).toISOString()}:${uuid()}`, expiresIn: 0 }, ); try { jwt.verify(token, publicKey); throw new Error('This token should not have verified') } catch (err) { expect((err as Error).message).to.equal('jwt expired'); } }); it('should allow us to decode and verify the content', () => { const jti = `${(new Date()).toISOString()}:${uuid()}`; const payload = { foo: 'bar' }; const token = jwt.sign( payload, { key: privateKey, passphrase: '' }, { algorithm: 'RS256', jwtid: jti, expiresIn: 1000 }, ); const decodedJwt = jwt.decode(token); expect(decodedJwt).to.include.keys(['iat', 'exp']); expect(decodedJwt).to.deep.include({ ...payload, jti, }) }); it('should not be vulnerable to content manipulation', () => { const jti = `${(new Date()).toISOString()}:${uuid()}`; const payload = { foo: 'bar' }; const token = jwt.sign( payload, { key: privateKey, passphrase: '' }, { algorithm: 'RS256', jwtid: jti, expiresIn: 0, noTimestamp: true }, ); const decodePayload = (str: string): string => ( Buffer.from(str, 'base64').toString('utf8') ); const encodePayload = (str: string) => Buffer .from(str, 'utf8') .toString('base64') .replace(/=/g, '') .replace(/\+/g, '-') .replace(/\//g, '_'); const splitToken = token.split('.'); const decodedJwtPayload = decodePayload(splitToken[1]); const hackedJwtPayload = decodedJwtPayload.replace( /"exp":[0-9]+/, `"exp":${(((new Date()).getTime() / 1000) + 1000).toFixed(0)}` ); const hackedJwt = [splitToken[0], encodePayload(hackedJwtPayload), splitToken[2]].join('.'); const decoded = jwt.decode(hackedJwt); decoded; try { jwt.verify(hackedJwt, publicKey); throw new Error('this jwt should not have verified against the signature') } catch (error) { expect((error as Error).message).to.equal('invalid signature') } }); });

Next