Skip to content

Commit e933da1

Browse files
chore(utxo-core): add error classes for util func
TICKET: BTC-2047 chore(utxo-core): renamed functions consistent and error prototypes TICKET: BTC-2047 chore(utxo-core): bip32utils to sign and verify message TICKET: BTC-2047 chore(utxo-core): added bitcoinjs-message back to package.json TICKET: BTC-2047 chore(utxo-core): refactored functions and debug tests TICKET: BTC-2047 chore(utxo-core): update verify function TICKET: BTC-2047 chore(utxo-core): updated test and verify TICKET: BTC-2047 chore(utxo-core): clean up rebase conflicts TICKET: BTC-2047 chore(utxo-core): added helper function to create attestation TICKET: BTC-2047
1 parent 636f8a9 commit e933da1

File tree

6 files changed

+146
-152
lines changed

6 files changed

+146
-152
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
export class ErrorNoPayGoProof extends Error {
2+
constructor(public outputIndex: number) {
3+
super(`There is no paygo address proof encoded in the PSBT at output ${outputIndex}.`);
4+
}
5+
}
6+
7+
export class ErrorMultiplePayGoProof extends Error {
8+
constructor() {
9+
super('There are multiple paygo address proofs encoded in the PSBT. Something went wrong.');
10+
}
11+
}
12+
13+
export class ErrorPayGoAddressProofFailedVerification extends Error {
14+
constructor() {
15+
super('Cannot verify the paygo address signature with the provided pubkey.');
16+
}
17+
}
18+
19+
export class ErrorOutputIndexOutOfBounds extends Error {
20+
constructor(public outputIndex: number) {
21+
super(`Output index ${outputIndex} is out of bounds for PSBT outputs.`);
22+
}
23+
}
24+
25+
export class ErrorMultiplePayGoProofAtPsbtIndex extends Error {
26+
constructor(public outputIndex: number) {
27+
super(`There are multiple PayGo addresses in the PSBT output ${outputIndex}.`);
28+
}
29+
}
Lines changed: 47 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,84 @@
11
import * as utxolib from '@bitgo/utxo-lib';
2-
import * as bitcoinMessage from 'bitcoinjs-message';
32
import { checkForOutput } from 'bip174/src/lib/utils';
43

5-
import { extractAddressBufferFromPayGoAttestationProof } from '../ExtractAddressPayGoAttestation';
4+
import { verifyMessage } from '../../bip32utils';
65

7-
/** The function consumes the signature as a parameter and adds the PayGo address to the
8-
* PSBT output at the output index where the signature is of the format:
9-
* 0x18Bitcoin Signed Message:\n<varint_length><ENTROPY><ADDRESS><UUID> signed by
10-
* the HSM beforehand.
6+
import {
7+
ErrorMultiplePayGoProof,
8+
ErrorMultiplePayGoProofAtPsbtIndex,
9+
ErrorNoPayGoProof,
10+
ErrorOutputIndexOutOfBounds,
11+
ErrorPayGoAddressProofFailedVerification,
12+
} from './Errors';
13+
14+
const NILLUUID = '00000000-0000-0000-0000-000000000000';
15+
16+
/** This function adds the entropy and signature into the PSBT output unknown key vals.
17+
* We store the entropy so that we reconstruct the message <ENTROPY><ADDRESS><UUID>
18+
* to later verify.
1119
*
1220
* @param psbt - PSBT that we need to encode our paygo address into
1321
* @param outputIndex - the index of the address in our output
1422
* @param sig - the signature that we want to encode
1523
*/
16-
export function addPaygoAddressProof(
24+
export function addPayGoAddressProof(
1725
psbt: utxolib.bitgo.UtxoPsbt,
1826
outputIndex: number,
1927
sig: Buffer,
20-
pub: Buffer
28+
entropy: Buffer
2129
): void {
2230
utxolib.bitgo.addProprietaryKeyValuesFromUnknownKeyValues(psbt, 'output', outputIndex, {
2331
key: {
2432
identifier: utxolib.bitgo.PSBT_PROPRIETARY_IDENTIFIER,
2533
subtype: utxolib.bitgo.ProprietaryKeySubtype.PAYGO_ADDRESS_ATTESTATION_PROOF,
26-
keydata: pub,
34+
keydata: entropy,
2735
},
2836
value: sig,
2937
});
3038
}
3139

32-
/** Verify the paygo address signature is valid using BitGoJs statics
40+
/** Verify the paygo address signature is valid using verification pub key.
3341
*
3442
* @param psbt - PSBT we want to verify that the paygo address is in
3543
* @param outputIndex - we have the output index that address is in
36-
* @param pub - The public key that we want to verify the proof with
37-
* @param message - The message we want to verify corresponding to sig
44+
* @param uuid
3845
* @returns
3946
*/
40-
export function verifyPaygoAddressProof(
47+
export function verifyPayGoAddressProof(
4148
psbt: utxolib.bitgo.UtxoPsbt,
4249
outputIndex: number,
43-
message: Buffer,
44-
attestationPubKey: Buffer
50+
verificationPubkey: Buffer
4551
): void {
4652
const psbtOutputs = checkForOutput(psbt.data.outputs, outputIndex);
4753
const stored = utxolib.bitgo.getProprietaryKeyValuesFromUnknownKeyValues(psbtOutputs, {
4854
identifier: utxolib.bitgo.PSBT_PROPRIETARY_IDENTIFIER,
4955
subtype: utxolib.bitgo.ProprietaryKeySubtype.PAYGO_ADDRESS_ATTESTATION_PROOF,
5056
});
51-
if (!stored) {
52-
throw new Error(`No address proof.`);
53-
}
5457

5558
// assert stored length is 0 or 1
5659
if (stored.length === 0) {
57-
throw new Error(`There is no paygo address proof encoded in the PSBT at output ${outputIndex}.`);
60+
throw new ErrorNoPayGoProof(outputIndex);
5861
} else if (stored.length > 1) {
59-
throw new Error('There are multiple paygo address proofs encoded in the PSBT. Something went wrong.');
62+
throw new ErrorMultiplePayGoProof();
6063
}
6164

65+
// We get the signature and entropy from our PSBT unknown key vals
6266
const signature = stored[0].value;
63-
const pub = stored[0].key.keydata;
64-
65-
// Check that the keydata pubkey is the same as the one we are verifying against
66-
if (Buffer.compare(pub, attestationPubKey) !== 0) {
67-
throw new Error('The public key in the PSBT does not match the provided public key.');
68-
}
69-
70-
// It doesn't matter that this is bitcoin or not, we just need to convert the public key buffer into an address format
71-
// for the verification
72-
const messageToVerify = utxolib.address.toBase58Check(
73-
utxolib.crypto.hash160(pub),
74-
utxolib.networks.bitcoin.pubKeyHash,
75-
utxolib.networks.bitcoin
76-
);
77-
78-
if (!bitcoinMessage.verify(message, messageToVerify, signature, utxolib.networks.bitcoin.messagePrefix)) {
79-
throw new Error('Cannot verify the paygo address signature with the provided pubkey.');
80-
}
81-
// We should be verifying the address that was encoded into our message.
82-
const addressFromProof = extractAddressBufferFromPayGoAttestationProof(message).toString();
67+
const entropy = stored[0].key.keydata;
8368

84-
// Check that the address from the proof matches what is in the PSBT
69+
// Get the the PayGo address from the txOutputs
8570
const txOutputs = psbt.txOutputs;
8671
if (outputIndex >= txOutputs.length) {
87-
throw new Error(`Output index ${outputIndex} is out of bounds for PSBT outputs.`);
72+
throw new ErrorOutputIndexOutOfBounds(outputIndex);
8873
}
8974
const output = txOutputs[outputIndex];
9075
const addressFromOutput = utxolib.address.fromOutputScript(output.script, psbt.network);
9176

92-
if (addressFromProof !== addressFromOutput) {
93-
throw new Error(
94-
`The address from the output (${addressFromOutput}) does not match the address that is in the proof (${addressFromProof}).`
95-
);
77+
// We construct our message <ENTROPY><ADDRESS><UUID>
78+
const message = createPayGoAttestationBuffer(addressFromOutput, entropy);
79+
80+
if (!verifyMessage(message.toString(), verificationPubkey, signature, utxolib.networks.bitcoin)) {
81+
throw new ErrorPayGoAddressProofFailedVerification();
9682
}
9783
}
9884

@@ -103,15 +89,15 @@ export function verifyPaygoAddressProof(
10389
* @param psbt
10490
* @returns number - the index of the output address
10591
*/
106-
export function getPaygoAddressProofOutputIndex(psbt: utxolib.bitgo.UtxoPsbt): number | undefined {
92+
export function getPayGoAddressProofOutputIndex(psbt: utxolib.bitgo.UtxoPsbt): number | undefined {
10793
const res = psbt.data.outputs.flatMap((output, outputIndex) => {
10894
const proprietaryKeyVals = utxolib.bitgo.getPsbtOutputProprietaryKeyVals(output, {
10995
identifier: utxolib.bitgo.PSBT_PROPRIETARY_IDENTIFIER,
11096
subtype: utxolib.bitgo.ProprietaryKeySubtype.PAYGO_ADDRESS_ATTESTATION_PROOF,
11197
});
11298

11399
if (proprietaryKeyVals.length > 1) {
114-
throw new Error(`There are multiple PayGo addresses in the PSBT output ${outputIndex}.`);
100+
throw new ErrorMultiplePayGoProofAtPsbtIndex(outputIndex);
115101
}
116102

117103
return proprietaryKeyVals.length === 0 ? [] : [outputIndex];
@@ -121,5 +107,17 @@ export function getPaygoAddressProofOutputIndex(psbt: utxolib.bitgo.UtxoPsbt): n
121107
}
122108

123109
export function psbtOutputIncludesPaygoAddressProof(psbt: utxolib.bitgo.UtxoPsbt): boolean {
124-
return getPaygoAddressProofOutputIndex(psbt) !== undefined;
110+
return getPayGoAddressProofOutputIndex(psbt) !== undefined;
111+
}
112+
113+
/** This function reconstructs the proof <ENTROPY><ADDRESS><UUID>
114+
* given the address and entropy.
115+
*
116+
* @param address
117+
* @param entropy
118+
* @returns
119+
*/
120+
export function createPayGoAttestationBuffer(address: string, entropy: Buffer): Buffer {
121+
const addressBuffer = Buffer.from(address);
122+
return Buffer.concat([entropy, addressBuffer, Buffer.from(NILLUUID)]);
125123
}

modules/utxo-core/src/testutil/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './fixtures.utils';
22
export * from './key.utils';
33
export * from './toPlainObject.utils';
44
export * from './generatePayGoAttestationProof.utils';
5+
export * from './parseVaspProof';
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as utxolib from '@bitgo/utxo-lib';
2+
3+
/** We receive a proof in the form:
4+
* 0x18Bitcoin Signed Message:\n<varint_length><ENTROPY><ADDRESS><UUID>
5+
* and when verifying our message in our PayGo utils we want to only verify
6+
* the message portion of our proof. This helps to pare our proof in that format,
7+
* and returns a Buffer.
8+
*
9+
* @param proof
10+
* @returns
11+
*/
12+
export function parseVaspProof(proof: Buffer): Buffer {
13+
const prefix = '\u0018Bitcoin Signed Message:\n';
14+
if (proof.toString().startsWith(prefix)) {
15+
proof = proof.slice(Buffer.from(prefix).length);
16+
utxolib.bufferutils.varuint.decode(proof, 0);
17+
// Determines how many bytes were consumed during our last varuint.decode(Buffer, offset)
18+
// So if varuint.decode(0xfd) then varuint.decode.bytes = 3
19+
// varuint.decode(0xfe) then varuint.decode.bytes = 5, etc.
20+
const varintBytesLength = utxolib.bufferutils.varuint.decode.bytes;
21+
22+
proof.slice(varintBytesLength);
23+
}
24+
return proof;
25+
}

modules/utxo-core/test/paygo/psbt/PayGoUtils.ts

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
import assert from 'assert';
2+
import crypto from 'crypto';
23

34
import * as utxolib from '@bitgo/utxo-lib';
4-
import * as bitcoinMessage from 'bitcoinjs-message';
55
import { decodeProprietaryKey } from 'bip174/src/lib/proprietaryKeyVal';
66
import { KeyValue } from 'bip174/src/lib/interfaces';
77
import { checkForOutput } from 'bip174/src/lib/utils';
88

99
import {
10-
addPaygoAddressProof,
11-
getPaygoAddressProofOutputIndex,
10+
addPayGoAddressProof,
11+
createPayGoAttestationBuffer,
12+
getPayGoAddressProofOutputIndex,
1213
psbtOutputIncludesPaygoAddressProof,
13-
verifyPaygoAddressProof,
14+
verifyPayGoAddressProof,
1415
} from '../../../src/paygo/psbt/PayGoUtils';
1516
import { generatePayGoAttestationProof } from '../../../src/testutil/generatePayGoAttestationProof.utils';
17+
import { parseVaspProof } from '../../../src/testutil/parseVaspProof';
18+
import { signMessage } from '../../../src/bip32utils';
1619

1720
// To construct our PSBTs
1821
const network = utxolib.networks.bitcoin;
@@ -35,7 +38,7 @@ const attestationPubKey = dummyPub1.user.publicKey;
3538
const attestationPrvKey = dummyPub1.user.privateKey!;
3639

3740
// UUID structure
38-
const nilUUID = '00000000-0000-0000-0000-000000000000';
41+
const nillUUID = '00000000-0000-0000-0000-000000000000';
3942

4043
// our xpub converted to base58 address
4144
const addressToVerify = utxolib.address.toBase58Check(
@@ -45,9 +48,13 @@ const addressToVerify = utxolib.address.toBase58Check(
4548
);
4649

4750
// this should be retuning a Buffer
48-
const addressProofBuffer = generatePayGoAttestationProof(nilUUID, Buffer.from(addressToVerify));
51+
const addressProofBuffer = generatePayGoAttestationProof(nillUUID, Buffer.from(addressToVerify));
52+
const addressProofMsgBuffer = parseVaspProof(addressProofBuffer);
53+
// We know that that the entropy is a set 64 bytes.
54+
const addressProofEntropy = addressProofMsgBuffer.subarray(0, 65);
55+
4956
// signature with the given msg addressProofBuffer
50-
const sig = bitcoinMessage.sign(addressProofBuffer, attestationPrvKey!, true, network.messagePrefix);
57+
const sig = signMessage(addressProofMsgBuffer.toString(), attestationPrvKey!, network);
5158

5259
function getTestPsbt() {
5360
return utxolib.testutil.constructPsbt(psbtInputs, psbtOutputs, network, rootWalletKeys, 'unsigned');
@@ -67,38 +74,25 @@ describe('addPaygoAddressProof and verifyPaygoAddressProof', () => {
6774
});
6875
}
6976

70-
it("should fail a proof verification if the proof isn't valid", () => {
71-
const outputIndex = 0;
72-
const psbt = getTestPsbt();
73-
addPaygoAddressProof(psbt, outputIndex, sig, Buffer.from(attestationPubKey));
74-
const output = checkForOutput(psbt.data.outputs, outputIndex);
75-
const proofInPsbt = getPaygoProprietaryKey(output.unknownKeyVals!);
76-
assert(proofInPsbt.length === 1);
77-
assert.throws(
78-
() => verifyPaygoAddressProof(psbt, 0, Buffer.from('Random Signed Message'), attestationPubKey),
79-
(e: any) => e.message === 'Cannot verify the paygo address signature with the provided pubkey.'
80-
);
81-
});
82-
8377
it('should add and verify a valid paygo address proof on the PSBT', () => {
8478
const psbt = getTestPsbt();
8579
psbt.addOutput({ script: utxolib.address.toOutputScript(addressToVerify, network), value: BigInt(10000) });
8680
const outputIndex = psbt.data.outputs.length - 1;
87-
addPaygoAddressProof(psbt, outputIndex, sig, Buffer.from(attestationPubKey));
88-
verifyPaygoAddressProof(psbt, outputIndex, Buffer.from(addressProofBuffer), attestationPubKey);
81+
addPayGoAddressProof(psbt, outputIndex, sig, addressProofEntropy);
82+
verifyPayGoAddressProof(psbt, outputIndex, attestationPubKey);
8983
});
9084

9185
it('should throw an error if there are multiple PayGo proprietary keys in the PSBT', () => {
9286
const outputIndex = 0;
9387
const psbt = getTestPsbt();
94-
addPaygoAddressProof(psbt, outputIndex, sig, Buffer.from(attestationPubKey));
95-
addPaygoAddressProof(psbt, outputIndex, Buffer.from('signature2'), Buffer.from('fakepubkey2s'));
88+
addPayGoAddressProof(psbt, outputIndex, sig, addressProofEntropy);
89+
addPayGoAddressProof(psbt, outputIndex, Buffer.from('signature2'), crypto.randomBytes(64));
9690
const output = checkForOutput(psbt.data.outputs, outputIndex);
9791
const proofInPsbt = getPaygoProprietaryKey(output.unknownKeyVals!);
9892
assert(proofInPsbt.length !== 0);
9993
assert(proofInPsbt.length > 1);
10094
assert.throws(
101-
() => verifyPaygoAddressProof(psbt, outputIndex, addressProofBuffer, attestationPubKey),
95+
() => verifyPayGoAddressProof(psbt, outputIndex, attestationPubKey),
10296
(e: any) => e.message === 'There are multiple paygo address proofs encoded in the PSBT. Something went wrong.'
10397
);
10498
});
@@ -108,7 +102,7 @@ describe('verifyPaygoAddressProof', () => {
108102
it('should throw an error if there is no PayGo address in PSBT', () => {
109103
const psbt = getTestPsbt();
110104
assert.throws(
111-
() => verifyPaygoAddressProof(psbt, 0, addressProofBuffer, attestationPubKey),
105+
() => verifyPayGoAddressProof(psbt, 0, attestationPubKey),
112106
(e: any) => e.message === 'There is no paygo address proof encoded in the PSBT at output 0.'
113107
);
114108
});
@@ -118,25 +112,43 @@ describe('getPaygoAddressProofIndex', () => {
118112
it('should get PayGo address proof index from PSBT if there is one', () => {
119113
const psbt = getTestPsbt();
120114
const outputIndex = 0;
121-
addPaygoAddressProof(psbt, outputIndex, sig, Buffer.from(attestationPubKey));
115+
addPayGoAddressProof(psbt, outputIndex, sig, Buffer.from(attestationPubKey));
122116
assert(psbtOutputIncludesPaygoAddressProof(psbt));
123-
assert(getPaygoAddressProofOutputIndex(psbt) === 0);
117+
assert(getPayGoAddressProofOutputIndex(psbt) === 0);
124118
});
125119

126120
it('should return undefined if there is no PayGo address proof in PSBT', () => {
127121
const psbt = getTestPsbt();
128-
assert(getPaygoAddressProofOutputIndex(psbt) === undefined);
122+
assert(getPayGoAddressProofOutputIndex(psbt) === undefined);
129123
assert(!psbtOutputIncludesPaygoAddressProof(psbt));
130124
});
131125

132126
it('should return an error and fail if we have multiple PayGo address in the PSBT in the same output index', () => {
133127
const psbt = getTestPsbt();
134128
const outputIndex = 0;
135-
addPaygoAddressProof(psbt, outputIndex, sig, Buffer.from(attestationPubKey));
136-
addPaygoAddressProof(psbt, outputIndex, sig, Buffer.from('xpub12345abcdef29a028510d3b2d4'));
129+
addPayGoAddressProof(psbt, outputIndex, sig, addressProofEntropy);
130+
addPayGoAddressProof(psbt, outputIndex, sig, crypto.randomBytes(64));
137131
assert.throws(
138-
() => getPaygoAddressProofOutputIndex(psbt),
132+
() => getPayGoAddressProofOutputIndex(psbt),
139133
(e: any) => e.message === 'There are multiple PayGo addresses in the PSBT output 0.'
140134
);
141135
});
142136
});
137+
138+
describe('createPayGoAttestationBuffer', () => {
139+
it('should create a PayGo Attestation proof matching with original proof', () => {
140+
const payGoAttestationProof = createPayGoAttestationBuffer(addressToVerify, addressProofEntropy);
141+
assert.strictEqual(payGoAttestationProof.toString(), addressProofMsgBuffer.toString());
142+
assert(Buffer.compare(payGoAttestationProof, addressProofMsgBuffer) === 0);
143+
});
144+
145+
it('should create a PayGo Attestation proof that does not match with different uuid', () => {
146+
const addressProofBufferDiffUuid = generatePayGoAttestationProof(
147+
'00000000-0000-0000-0000-000000000001',
148+
Buffer.from(addressToVerify)
149+
);
150+
const payGoAttestationProof = createPayGoAttestationBuffer(addressToVerify, addressProofEntropy);
151+
assert.notStrictEqual(payGoAttestationProof.toString(), addressProofBufferDiffUuid.toString());
152+
assert(Buffer.compare(payGoAttestationProof, addressProofBufferDiffUuid) !== 0);
153+
});
154+
});

0 commit comments

Comments
 (0)