Skip to content

Commit

Permalink
fix: correctly verify future message signing prefix
Browse files Browse the repository at this point in the history
  • Loading branch information
janniks committed Sep 28, 2022
1 parent 889fa22 commit d27e054
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 14 deletions.
14 changes: 11 additions & 3 deletions packages/encryption/src/ec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
import { createCipher } from './aesCipher';
import { createHmacSha256 } from './hmacSha256';
import { getPublicKeyFromPrivate } from './keys';
import { hashMessage } from './messageSignature';
import { encodeMessage, hashMessage } from './messageSignature';
import { hashSha256Sync, hashSha512Sync } from './sha2Hash';
import { getAesCbcOutputLength, getBase64OutputLength } from './utils';

Expand Down Expand Up @@ -525,7 +525,7 @@ interface VerifyMessageSignatureArgs {
}

/**
* Verify message signature with recoverable public key
* Verify message signature (VRS format) with recoverable public key
* @deprecated The Clarity compatible {@link verifyMessageSignatureRsv} is preferred
*/
export function verifyMessageSignature({
Expand All @@ -540,7 +540,15 @@ export function verifyMessageSignature({
// verify() is strict: true by default. High-s signatures are rejected, which mirrors libsecp behavior
// Set verify options to strict: false, to support the legacy stacks implementations
// Reference: https://github.com/paulmillr/noble-secp256k1/releases/tag/1.4.0
return verify(sig, hashedMsg, publicKey, { strict: false });
const legacyResult = verify(sig, hashedMsg, publicKey, { strict: false });

// Temporary Additional Check ++++++++++++++++++++++++++++++++++++++++++++++++
if (legacyResult || typeof message !== 'string') return legacyResult;

const FUTURE_PREFIX = '\x17Stacks Signed Message:\n';
const futureHash = Buffer.from(sha256(encodeMessage(message, FUTURE_PREFIX)));
return verify(sig, futureHash, publicKey, { strict: false });
// +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
}

/**
Expand Down
6 changes: 3 additions & 3 deletions packages/encryption/src/messageSignature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import { decode, encode, encodingLength } from 'varuint-bitcoin';

// 'Stacks Message Signing:\n'.length // = 24
// 'Stacks Message Signing:\n'.length.toString(16) // = 18
const chainPrefix = '\x18Stacks Message Signing:\n';
const chainPrefix: string = '\x18Stacks Message Signing:\n';

export function hashMessage(message: string): Buffer {
return Buffer.from(sha256(encodeMessage(message)));
}

export function encodeMessage(message: string | Buffer): Buffer {
export function encodeMessage(message: string | Buffer, prefix: string = chainPrefix): Buffer {
const encoded = encode(Buffer.from(message).length);
return Buffer.concat([Buffer.from(chainPrefix), encoded, Buffer.from(message)]);
return Buffer.concat([Buffer.from(prefix), encoded, Buffer.from(message)]);
}

export function decodeMessage(encodedMessage: Buffer): Buffer {
Expand Down
14 changes: 8 additions & 6 deletions packages/encryption/tests/messageSignature.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,26 @@ test('encodeMessage / decodeMessage', () => {
['hello world', '\x18Stacks Message Signing:\n\x0bhello world'],
['', '\x18Stacks Message Signing:\n\x00'],
// Longer message (to test a different length for the var_int prefix)
['This is a really long message to test the var_int prefix This is a really long message to test the var_int prefix This is a really long message to test the var_int prefix This is a really long message to test the var_int prefix This is a really long message to test the var_int prefix',
[
'This is a really long message to test the var_int prefix This is a really long message to test the var_int prefix This is a really long message to test the var_int prefix This is a really long message to test the var_int prefix This is a really long message to test the var_int prefix',
Buffer.concat([
Buffer.from('\x18Stacks Message Signing:\n'),
// message length = 284 (decimal) = 011c (hex) <=> \x1c\x01 (little endian encoding)
// Since length = 284 is < 0xFFFF, prefix the int with 0xFD followed by 2 bytes for a total of 3 bytes (see https://en.bitcoin.it/wiki/Protocol_documentation#Variable_length_integer)
Buffer.from(new Uint8Array([253, 28, 1])),
Buffer.from('This is a really long message to test the var_int prefix This is a really long message to test the var_int prefix This is a really long message to test the var_int prefix This is a really long message to test the var_int prefix This is a really long message to test the var_int prefix')
])],
]
Buffer.from(
'This is a really long message to test the var_int prefix This is a really long message to test the var_int prefix This is a really long message to test the var_int prefix This is a really long message to test the var_int prefix This is a really long message to test the var_int prefix'
),
]),
],
];
for (let messageArr of messages) {
const [message, expectedEncodedMessage] = messageArr;
const encodedMessage = encodeMessage(message);
expect(encodedMessage.equals(Buffer.from(expectedEncodedMessage))).toBeTruthy();
const decodedMessage = decodeMessage(encodedMessage);
expect(decodedMessage.toString()).toEqual(message);
}


});

test('hash message vs hash of manually constructed message', () => {
Expand Down
35 changes: 33 additions & 2 deletions packages/transactions/tests/structuredDataSignature.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { sha256 } from '@noble/hashes/sha256';
import { standardPrincipalCV, stringAsciiCV, trueCV, tupleCV, uintCV } from '../src/clarity';
import { createStacksPrivateKey } from '../src/keys';
import { Buffer } from '@stacks/common';
import { verifyMessageSignatureRsv } from '@stacks/encryption';
import { standardPrincipalCV, stringAsciiCV, trueCV, tupleCV, uintCV } from '../src/clarity';
import { createStacksPrivateKey, publicKeyFromSignatureRsv, signMessageHashRsv } from '../src/keys';
import {
decodeStructuredDataSignature,
encodeStructuredData,
Expand Down Expand Up @@ -240,3 +241,33 @@ describe('SIP018 test vectors', () => {
expect(isSignatureVerified).toBe(true);
});
});

test('verifyMessageSignature works for both legacy/current and future message signing prefixes', () => {
const privateKey = '3b444e0e243d2faccaf8ecf1dcb4aeac98e122c79b4df3eb3cc8cec3768dbe8e';

// taken from other tests via `openssl`
const message = 'hello world';
const encodedMessageHash = '664d1478d36935361c1a8eda75fce73c49a93b58e55ed7cb45c3860317814991';
const encodedMessageHashAlt = '619997693db23de4b92ed152444a578a134143d9ad2c0f4dff2615de9d42ad96';

const signature = signMessageHashRsv({
messageHash: encodedMessageHash,
privateKey: createStacksPrivateKey(privateKey),
});
const signatureAlt = signMessageHashRsv({
messageHash: encodedMessageHashAlt,
privateKey: createStacksPrivateKey(privateKey),
});

const publicKey = publicKeyFromSignatureRsv(encodedMessageHash, signature);
const publicKeyAlt = publicKeyFromSignatureRsv(encodedMessageHashAlt, signatureAlt);

expect(verifyMessageSignatureRsv({ message, signature: signature.data, publicKey })).toBe(true);
expect(
verifyMessageSignatureRsv({
message,
signature: signatureAlt.data,
publicKey: publicKeyAlt,
})
).toBe(true);
});

1 comment on commit d27e054

@vercel
Copy link

@vercel vercel bot commented on d27e054 Sep 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.