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
19 changes: 19 additions & 0 deletions modules/utxo-core/src/paygo/attestation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import assert from 'assert';

import * as utxolib from '@bitgo/utxo-lib';
export const NIL_UUID = '00000000-0000-0000-0000-000000000000';

/** This function reconstructs the proof <ENTROPY><ADDRESS><UUID>
* given the address and entropy.
*
* @param address
* @param entropy
* @returns
*/
export function createPayGoAttestationBuffer(address: string, entropy: Buffer, network: utxolib.Network): Buffer {
assert(address.length > 0);
const isValidAddress = utxolib.address.toOutputScript(address, network);
assert(isValidAddress, `Address ${address} is not a valid address.`);
const addressBuffer = Buffer.from(address);
return Buffer.concat([entropy, addressBuffer, Buffer.from(NIL_UUID)]);
}
1 change: 1 addition & 0 deletions modules/utxo-core/src/paygo/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './ExtractAddressPayGoAttestation';
export * from './psbt';
36 changes: 36 additions & 0 deletions modules/utxo-core/src/paygo/psbt/Errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
export class PayGoError extends Error {
constructor(message: string) {
super(message);
this.name = this.constructor.name;
}
}

export class ErrorNoPayGoProof extends PayGoError {
constructor(public outputIndex: number) {
super(`There is no paygo address proof encoded in the PSBT at output ${outputIndex}.`);
}
}

export class ErrorMultiplePayGoProof extends PayGoError {
constructor() {
super('There are multiple paygo address proofs encoded in the PSBT. Something went wrong.');
}
}

export class ErrorPayGoAddressProofFailedVerification extends PayGoError {
constructor() {
super('Cannot verify the paygo address signature with the provided pubkey.');
}
}

export class ErrorOutputIndexOutOfBounds extends PayGoError {
constructor(public outputIndex: number) {
super(`Output index ${outputIndex} is out of bounds for PSBT outputs.`);
}
}

export class ErrorMultiplePayGoProofAtPsbtIndex extends PayGoError {
constructor(public outputIndex: number) {
super(`There are multiple PayGo addresses in the PSBT output ${outputIndex}.`);
}
}
1 change: 1 addition & 0 deletions modules/utxo-core/src/paygo/psbt/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './payGoAddressProof';
110 changes: 110 additions & 0 deletions modules/utxo-core/src/paygo/psbt/payGoAddressProof.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import * as utxolib from '@bitgo/utxo-lib';
import { checkForOutput } from 'bip174/src/lib/utils';

import { verifyMessage } from '../../bip32utils';
import { createPayGoAttestationBuffer } from '../attestation';

import {
ErrorMultiplePayGoProof,
ErrorMultiplePayGoProofAtPsbtIndex,
ErrorNoPayGoProof,
ErrorOutputIndexOutOfBounds,
ErrorPayGoAddressProofFailedVerification,
} from './Errors';

/** This function adds the entropy and signature into the PSBT output unknown key vals.
* We store the entropy so that we reconstruct the message <ENTROPY><ADDRESS><UUID>
* to later verify.
*
* @param psbt - PSBT that we need to encode our paygo address into
* @param outputIndex - the index of the address in our output
* @param sig - the signature that we want to encode
*/
export function addPayGoAddressProof(
psbt: utxolib.bitgo.UtxoPsbt,
outputIndex: number,
sig: Buffer,
entropy: Buffer
): void {
utxolib.bitgo.addProprietaryKeyValuesFromUnknownKeyValues(psbt, 'output', outputIndex, {
key: {
identifier: utxolib.bitgo.PSBT_PROPRIETARY_IDENTIFIER,
subtype: utxolib.bitgo.ProprietaryKeySubtype.PAYGO_ADDRESS_ATTESTATION_PROOF,
keydata: entropy,
},
value: sig,
});
}

/** Verify the paygo address signature is valid using verification pub key.
*
* @param psbt - PSBT we want to verify that the paygo address is in
* @param outputIndex - we have the output index that address is in
* @param uuid
* @returns
*/
export function verifyPayGoAddressProof(
psbt: utxolib.bitgo.UtxoPsbt,
outputIndex: number,
verificationPubkey: Buffer
): void {
const psbtOutputs = checkForOutput(psbt.data.outputs, outputIndex);
const stored = utxolib.bitgo.getProprietaryKeyValuesFromUnknownKeyValues(psbtOutputs, {
identifier: utxolib.bitgo.PSBT_PROPRIETARY_IDENTIFIER,
subtype: utxolib.bitgo.ProprietaryKeySubtype.PAYGO_ADDRESS_ATTESTATION_PROOF,
});

// assert stored length is 0 or 1
if (stored.length === 0) {
throw new ErrorNoPayGoProof(outputIndex);
} else if (stored.length > 1) {
throw new ErrorMultiplePayGoProof();
}

// We get the signature and entropy from our PSBT unknown key vals
const signature = stored[0].value;
const entropy = stored[0].key.keydata;

// Get the the PayGo address from the txOutputs
const txOutputs = psbt.txOutputs;
if (outputIndex >= txOutputs.length) {
throw new ErrorOutputIndexOutOfBounds(outputIndex);
}
const output = txOutputs[outputIndex];
const addressFromOutput = utxolib.address.fromOutputScript(output.script, psbt.network);

// We construct our message <ENTROPY><ADDRESS><UUID>
const message = createPayGoAttestationBuffer(addressFromOutput, entropy, psbt.network);

if (!verifyMessage(message.toString(), verificationPubkey, signature, utxolib.networks.bitcoin)) {
throw new ErrorPayGoAddressProofFailedVerification();
}
}

/** Get the output index of the paygo output if there is one. It does this by
* checking if the metadata is on one of the outputs of the PSBT. If there is
* no paygo output, return undefined
*
* @param psbt
* @returns number - the index of the output address
*/
export function getPayGoAddressProofOutputIndex(psbt: utxolib.bitgo.UtxoPsbt): number | undefined {
const res = psbt.data.outputs.flatMap((output, outputIndex) => {
const proprietaryKeyVals = utxolib.bitgo.getPsbtOutputProprietaryKeyVals(output, {
identifier: utxolib.bitgo.PSBT_PROPRIETARY_IDENTIFIER,
subtype: utxolib.bitgo.ProprietaryKeySubtype.PAYGO_ADDRESS_ATTESTATION_PROOF,
});

if (proprietaryKeyVals.length > 1) {
throw new ErrorMultiplePayGoProofAtPsbtIndex(outputIndex);
}

return proprietaryKeyVals.length === 0 ? [] : [outputIndex];
});

return res.length === 0 ? undefined : res[0];
}

export function psbtOutputIncludesPaygoAddressProof(psbt: utxolib.bitgo.UtxoPsbt): boolean {
return getPayGoAddressProofOutputIndex(psbt) !== undefined;
}
1 change: 1 addition & 0 deletions modules/utxo-core/src/testutil/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './fixtures.utils';
export * from './key.utils';
export * from './toPlainObject.utils';
export * from './generatePayGoAttestationProof.utils';
export * from './trimMessagePrefix';
25 changes: 25 additions & 0 deletions modules/utxo-core/src/testutil/trimMessagePrefix.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import * as utxolib from '@bitgo/utxo-lib';

/** We receive a proof in the form:
* 0x18Bitcoin Signed Message:\n<varint_length><ENTROPY><ADDRESS><UUID>
* and when verifying our message we want to exclude the 0x18Bitcoin Signed Message:\n<varint_length>
* of the proof so that we are left with the entropy address and uuid as our message.
* This is what we are going to be verifying.
*
* @param proof
* @returns
*/
export function trimMessagePrefix(proof: Buffer): Buffer {
const prefix = '\u0018Bitcoin Signed Message:\n';
if (proof.toString().startsWith(prefix)) {
proof = proof.slice(Buffer.from(prefix).length);
utxolib.bufferutils.varuint.decode(proof, 0);
// Determines how many bytes were consumed during our last varuint.decode(Buffer, offset)
// So if varuint.decode(0xfd) then varuint.decode.bytes = 3
// varuint.decode(0xfe) then varuint.decode.bytes = 5, etc.
const varintBytesLength = utxolib.bufferutils.varuint.decode.bytes;

proof.slice(varintBytesLength);
}
return proof;
}
24 changes: 24 additions & 0 deletions modules/utxo-core/test/paygo/attestation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import assert from 'assert';

import { createPayGoAttestationBuffer } from '../../src/paygo/attestation';
import { generatePayGoAttestationProof } from '../../src/testutil';

import { addressProofEntropy, addressProofMsgBuffer, addressToVerify, network } from './psbt/payGoAddressProof';

describe('createPayGoAttestationBuffer', () => {
it('should create a PayGo Attestation proof matching with original proof', () => {
const payGoAttestationProof = createPayGoAttestationBuffer(addressToVerify, addressProofEntropy, network);
assert.strictEqual(payGoAttestationProof.toString(), addressProofMsgBuffer.toString());
assert(Buffer.compare(payGoAttestationProof, addressProofMsgBuffer) === 0);
});

it('should create a PayGo Attestation proof that does not match with different uuid', () => {
const addressProofBufferDiffUuid = generatePayGoAttestationProof(
'00000000-0000-0000-0000-000000000001',
Buffer.from(addressToVerify)
);
const payGoAttestationProof = createPayGoAttestationBuffer(addressToVerify, addressProofEntropy, network);
assert.notStrictEqual(payGoAttestationProof.toString(), addressProofBufferDiffUuid.toString());
assert(Buffer.compare(payGoAttestationProof, addressProofBufferDiffUuid) !== 0);
});
});
133 changes: 133 additions & 0 deletions modules/utxo-core/test/paygo/psbt/payGoAddressProof.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import assert from 'assert';
import crypto from 'crypto';

import * as utxolib from '@bitgo/utxo-lib';
import { decodeProprietaryKey } from 'bip174/src/lib/proprietaryKeyVal';
import { KeyValue } from 'bip174/src/lib/interfaces';
import { checkForOutput } from 'bip174/src/lib/utils';

import {
addPayGoAddressProof,
getPayGoAddressProofOutputIndex,
psbtOutputIncludesPaygoAddressProof,
verifyPayGoAddressProof,
} from '../../../src/paygo/psbt/payGoAddressProof';
import { generatePayGoAttestationProof } from '../../../src/testutil/generatePayGoAttestationProof.utils';
import { trimMessagePrefix } from '../../../src/testutil/trimMessagePrefix';
import { signMessage } from '../../../src/bip32utils';
import { NIL_UUID } from '../../../src/paygo/attestation';

// To construct our PSBTs
export const network = utxolib.networks.bitcoin;
const keys = [1, 2, 3].map((v) => utxolib.bip32.fromSeed(Buffer.alloc(16, `test/2/${v}`), network));
const rootWalletKeys = new utxolib.bitgo.RootWalletKeys([keys[0], keys[1], keys[2]]);

// PSBT INPUTS AND OUTPUTS
const psbtInputs = utxolib.testutil.inputScriptTypes.map((scriptType) => ({
scriptType,
value: BigInt(1000),
}));
const psbtOutputs = utxolib.testutil.outputScriptTypes.map((scriptType) => ({
scriptType,
value: BigInt(900),
}));

// wallet pub and priv key for tbtc
const dummyPub1 = rootWalletKeys.deriveForChainAndIndex(50, 200);
export const attestationPubKey = dummyPub1.user.publicKey;
export const attestationPrvKey = dummyPub1.user.privateKey!;

// our xpub converted to base58 address
export const addressToVerify = utxolib.address.toBase58Check(
utxolib.crypto.hash160(Buffer.from(dummyPub1.backup.publicKey)),
utxolib.networks.bitcoin.pubKeyHash,
utxolib.networks.bitcoin
);

// this should be retuning a Buffer
export const addressProofBuffer = generatePayGoAttestationProof(NIL_UUID, Buffer.from(addressToVerify));
export const addressProofMsgBuffer = trimMessagePrefix(addressProofBuffer);
// We know that that the entropy is a set 64 bytes.
export const addressProofEntropy = addressProofMsgBuffer.subarray(0, 65);

// signature with the given msg addressProofBuffer
export const sig = signMessage(addressProofMsgBuffer.toString(), attestationPrvKey!, network);

function getTestPsbt() {
return utxolib.testutil.constructPsbt(psbtInputs, psbtOutputs, network, rootWalletKeys, 'unsigned');
}

describe('addPaygoAddressProof and verifyPaygoAddressProof', () => {
function getPayGoProprietaryKey(proprietaryKeyVals: KeyValue[]) {
return proprietaryKeyVals
.map(({ key, value }) => {
return { key: decodeProprietaryKey(key), value };
})
.filter((keyValue) => {
return (
keyValue.key.identifier === utxolib.bitgo.PSBT_PROPRIETARY_IDENTIFIER &&
keyValue.key.subtype === utxolib.bitgo.ProprietaryKeySubtype.PAYGO_ADDRESS_ATTESTATION_PROOF
);
});
}

it('should add and verify a valid paygo address proof on the PSBT', () => {
const psbt = getTestPsbt();
psbt.addOutput({ script: utxolib.address.toOutputScript(addressToVerify, network), value: BigInt(10000) });
const outputIndex = psbt.data.outputs.length - 1;
addPayGoAddressProof(psbt, outputIndex, sig, addressProofEntropy);
verifyPayGoAddressProof(psbt, outputIndex, attestationPubKey);
});

it('should throw an error if there are multiple PayGo proprietary keys in the PSBT', () => {
const outputIndex = 0;
const psbt = getTestPsbt();
addPayGoAddressProof(psbt, outputIndex, sig, addressProofEntropy);
addPayGoAddressProof(psbt, outputIndex, Buffer.from('signature2'), crypto.randomBytes(64));
const output = checkForOutput(psbt.data.outputs, outputIndex);
const proofInPsbt = getPayGoProprietaryKey(output.unknownKeyVals!);
assert(proofInPsbt.length !== 0);
assert(proofInPsbt.length > 1);
assert.throws(
() => verifyPayGoAddressProof(psbt, outputIndex, attestationPubKey),
(e: any) => e.message === 'There are multiple paygo address proofs encoded in the PSBT. Something went wrong.'
);
});
});

describe('verifyPaygoAddressProof', () => {
it('should throw an error if there is no PayGo address in PSBT', () => {
const psbt = getTestPsbt();
assert.throws(
() => verifyPayGoAddressProof(psbt, 0, attestationPubKey),
(e: any) => e.message === 'There is no paygo address proof encoded in the PSBT at output 0.'
);
});
});

describe('getPaygoAddressProofIndex', () => {
it('should get PayGo address proof index from PSBT if there is one', () => {
const psbt = getTestPsbt();
const outputIndex = 0;
addPayGoAddressProof(psbt, outputIndex, sig, Buffer.from(attestationPubKey));
assert(psbtOutputIncludesPaygoAddressProof(psbt));
assert(getPayGoAddressProofOutputIndex(psbt) === 0);
});

it('should return undefined if there is no PayGo address proof in PSBT', () => {
const psbt = getTestPsbt();
assert(getPayGoAddressProofOutputIndex(psbt) === undefined);
assert(!psbtOutputIncludesPaygoAddressProof(psbt));
});

it('should return an error and fail if we have multiple PayGo address in the PSBT in the same output index', () => {
const psbt = getTestPsbt();
const outputIndex = 0;
addPayGoAddressProof(psbt, outputIndex, sig, addressProofEntropy);
addPayGoAddressProof(psbt, outputIndex, sig, crypto.randomBytes(64));
assert.throws(
() => getPayGoAddressProofOutputIndex(psbt),
(e: any) => e.message === 'There are multiple PayGo addresses in the PSBT output 0.'
);
});
});
1 change: 1 addition & 0 deletions modules/utxo-lib/src/bitgo/PsbtUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export enum ProprietaryKeySubtype {
MUSIG2_PARTICIPANT_PUB_KEYS = 0x01,
MUSIG2_PUB_NONCE = 0x02,
MUSIG2_PARTIAL_SIG = 0x03,
PAYGO_ADDRESS_ATTESTATION_PROOF = 0x04,
}

/**
Expand Down
4 changes: 2 additions & 2 deletions modules/utxo-lib/src/bitgo/zcash/address.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export function toBase58Check(hash: Buffer, version: number): string {
}

export function fromOutputScript(outputScript: Buffer, network: Network): string {
assert(isZcash(network));
assert.ok(isZcash(network));
let o;
let prefix;
try {
Expand All @@ -41,7 +41,7 @@ export function fromOutputScript(outputScript: Buffer, network: Network): string
}

export function toOutputScript(address: string, network: Network): Buffer {
assert(isZcash(network));
assert.ok(isZcash(network));
const { version, hash } = fromBase58Check(address);
if (version === network.pubKeyHash) {
return payments.p2pkh({ hash }).output as Buffer;
Expand Down