Detect and verify wrapped messages (#1110)
* Detect and verify wrapped messages * Expose isWrapped detection * Complexity cleanups * Update wrap.spec.ts * Update wrap.ts * Extract fn getter * Test for wrapping to detect path
This commit is contained in:
@@ -4,6 +4,8 @@
|
||||
|
||||
Changes:
|
||||
|
||||
- Add `u8aWrapBytes` & `u8aUnwrapBytes` (originally from `@polkadot/extension-dapp`)
|
||||
- Verify both wrapped and unwrapped signatures transparently
|
||||
- Adjust `isHex` return to `0x${string}` under TypeScript
|
||||
- Adjust shared repo Jest config
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2017-2021 @polkadot/util-crypto authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { hexToU8a, stringToU8a, u8aConcat, u8aToHex } from '@polkadot/util';
|
||||
import { hexToU8a, stringToU8a, u8aConcat, u8aToHex, u8aWrapBytes } from '@polkadot/util';
|
||||
import { waitReady } from '@polkadot/wasm-crypto';
|
||||
|
||||
import { decodeAddress } from '../address';
|
||||
@@ -9,11 +9,13 @@ import { signatureVerify } from '.';
|
||||
|
||||
const ADDR_ED = 'DxN4uvzwPzJLtn17yew6jEffPhXQfdKHTp2brufb98vGbPN';
|
||||
const ADDR_SR = 'EK1bFgKm2FsghcttHT7TB7rNyXApFgs9fCbijMGQNyFGBQm';
|
||||
const ADDR_SR_WRAP = 'J9nD3s7zssCX7bion1xctAF6xcVexcpy2uwy4jTm9JL8yuK';
|
||||
const ADDR_EC = 'XyFVXiGaHxoBhXZkSh6NS2rjFyVaVNUo5UiZDqZbuSfUdji';
|
||||
const ADDR_ET = '0x54Dab85EE2c7b9F7421100d7134eFb5DfA4239bF';
|
||||
const MESSAGE = 'hello world';
|
||||
const SIG_ED = '0x299d3bf4c8bb51af732f8067b3a3015c0862a5ff34721749d8ed6577ea2708365d1c5f76bd519009971e41156f12c70abc2533837ceb3bad9a05a99ab923de06';
|
||||
const SIG_SR = '0xca01419b5a17219f7b78335658cab3b126db523a5df7be4bfc2bef76c2eb3b1dcf4ca86eb877d0a6cf6df12db5995c51d13b00e005d053b892bd09c594434288';
|
||||
const SIG_SR_WRAP = '0x84b6afb1c8e54bbcb3f4872baf172580e21310e9387a53742627d6652d121447fa406b82805ed3184fb7bd519175cc9f99f283f97954d95cf966ee164df85489';
|
||||
const SIG_EC = '0x994638ee586d2c5dbd9bacacbc35d9b7e9018de8f7892f00c900db63bc57b1283e2ee7bc51a9b1c1dae121ac4f4b9e2a41cd1d6bf4bb3e24d7fed6faf6d85e0501';
|
||||
const SIG_ET = '0x4e35aad35793b71f08566615661c9b741d7c605bc8935ac08608dff685324d71b5704fbd14c9297d2f584ea0735f015dcf0def66b802b3f555e1db916eda4b7700';
|
||||
const MUL_ED = u8aToHex(u8aConcat(new Uint8Array([0]), hexToU8a(SIG_ED)));
|
||||
@@ -37,6 +39,7 @@ describe('signatureVerify', (): void => {
|
||||
expect(signatureVerify(MESSAGE, SIG_ED, ADDR_ED)).toEqual({
|
||||
crypto: 'ed25519',
|
||||
isValid: true,
|
||||
isWrapped: false,
|
||||
publicKey: decodeAddress(ADDR_ED)
|
||||
});
|
||||
});
|
||||
@@ -45,6 +48,7 @@ describe('signatureVerify', (): void => {
|
||||
expect(signatureVerify(MESSAGE, SIG_EC, ADDR_EC)).toEqual({
|
||||
crypto: 'ecdsa',
|
||||
isValid: true,
|
||||
isWrapped: false,
|
||||
publicKey: decodeAddress(ADDR_EC)
|
||||
});
|
||||
});
|
||||
@@ -53,6 +57,7 @@ describe('signatureVerify', (): void => {
|
||||
expect(signatureVerify(MESSAGE, SIG_ET, ADDR_ET)).toEqual({
|
||||
crypto: 'ethereum',
|
||||
isValid: true,
|
||||
isWrapped: false,
|
||||
publicKey: hexToU8a(ADDR_ET)
|
||||
});
|
||||
});
|
||||
@@ -67,6 +72,7 @@ describe('signatureVerify', (): void => {
|
||||
)).toEqual({
|
||||
crypto: 'ethereum',
|
||||
isValid: true,
|
||||
isWrapped: true,
|
||||
publicKey: hexToU8a('0x002309df96687e44280bb72c3818358faeeb699c')
|
||||
});
|
||||
});
|
||||
@@ -75,6 +81,7 @@ describe('signatureVerify', (): void => {
|
||||
expect(signatureVerify(MESSAGE, SIG_EC, ADDR_ET)).toEqual({
|
||||
crypto: 'none',
|
||||
isValid: false,
|
||||
isWrapped: false,
|
||||
publicKey: hexToU8a(ADDR_ET)
|
||||
});
|
||||
});
|
||||
@@ -83,14 +90,34 @@ describe('signatureVerify', (): void => {
|
||||
expect(signatureVerify(MESSAGE, SIG_SR, ADDR_SR)).toEqual({
|
||||
crypto: 'sr25519',
|
||||
isValid: true,
|
||||
isWrapped: false,
|
||||
publicKey: decodeAddress(ADDR_SR)
|
||||
});
|
||||
});
|
||||
|
||||
it('verifies an sr25519 signature (with msg wrapper, without wrapped sig)', (): void => {
|
||||
expect(signatureVerify(u8aWrapBytes(MESSAGE), SIG_SR_WRAP, ADDR_SR_WRAP)).toEqual({
|
||||
crypto: 'sr25519',
|
||||
isValid: true,
|
||||
isWrapped: true,
|
||||
publicKey: decodeAddress(ADDR_SR_WRAP)
|
||||
});
|
||||
});
|
||||
|
||||
it('verifies an sr25519 signature (without msg wrapper, with wrapped sig)', (): void => {
|
||||
expect(signatureVerify(MESSAGE, SIG_SR_WRAP, ADDR_SR_WRAP)).toEqual({
|
||||
crypto: 'sr25519',
|
||||
isValid: true,
|
||||
isWrapped: false,
|
||||
publicKey: decodeAddress(ADDR_SR_WRAP)
|
||||
});
|
||||
});
|
||||
|
||||
it('allows various inputs', (): void => {
|
||||
expect(signatureVerify(stringToU8a(MESSAGE), hexToU8a(SIG_ED), decodeAddress(ADDR_ED))).toEqual({
|
||||
crypto: 'ed25519',
|
||||
isValid: true,
|
||||
isWrapped: false,
|
||||
publicKey: decodeAddress(ADDR_ED)
|
||||
});
|
||||
});
|
||||
@@ -99,6 +126,7 @@ describe('signatureVerify', (): void => {
|
||||
expect(signatureVerify(MESSAGE, SIG_SR, ADDR_ED)).toEqual({
|
||||
crypto: 'none',
|
||||
isValid: false,
|
||||
isWrapped: false,
|
||||
publicKey: decodeAddress(ADDR_ED)
|
||||
});
|
||||
});
|
||||
@@ -109,6 +137,7 @@ describe('signatureVerify', (): void => {
|
||||
expect(signatureVerify(MESSAGE, MUL_ED, ADDR_ED)).toEqual({
|
||||
crypto: 'ed25519',
|
||||
isValid: true,
|
||||
isWrapped: false,
|
||||
publicKey: decodeAddress(ADDR_ED)
|
||||
});
|
||||
});
|
||||
@@ -117,6 +146,7 @@ describe('signatureVerify', (): void => {
|
||||
expect(signatureVerify(MESSAGE, MUL_EC, ADDR_EC)).toEqual({
|
||||
crypto: 'ecdsa',
|
||||
isValid: true,
|
||||
isWrapped: false,
|
||||
publicKey: decodeAddress(ADDR_EC)
|
||||
});
|
||||
});
|
||||
@@ -125,6 +155,7 @@ describe('signatureVerify', (): void => {
|
||||
expect(signatureVerify(MESSAGE, MUL_ET, ADDR_ET)).toEqual({
|
||||
crypto: 'ethereum',
|
||||
isValid: true,
|
||||
isWrapped: false,
|
||||
publicKey: hexToU8a(ADDR_ET)
|
||||
});
|
||||
});
|
||||
@@ -133,6 +164,7 @@ describe('signatureVerify', (): void => {
|
||||
expect(signatureVerify(MESSAGE, MUL_SR, ADDR_SR)).toEqual({
|
||||
crypto: 'sr25519',
|
||||
isValid: true,
|
||||
isWrapped: false,
|
||||
publicKey: decodeAddress(ADDR_SR)
|
||||
});
|
||||
});
|
||||
@@ -141,6 +173,7 @@ describe('signatureVerify', (): void => {
|
||||
expect(signatureVerify(MESSAGE, MUL_SR, ADDR_ED)).toEqual({
|
||||
crypto: 'sr25519',
|
||||
isValid: false,
|
||||
isWrapped: false,
|
||||
publicKey: new Uint8Array([61, 12, 55, 211, 0, 211, 97, 199, 4, 37, 17, 213, 81, 175, 166, 23, 251, 199, 144, 210, 19, 83, 186, 1, 196, 231, 14, 156, 171, 46, 141, 146])
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import type { HexString } from '@polkadot/util/types';
|
||||
import type { KeypairType, VerifyResult } from '../types';
|
||||
|
||||
import { assert, u8aToU8a } from '@polkadot/util';
|
||||
import { assert, u8aIsWrapped, u8aToU8a, u8aUnwrapBytes, u8aWrapBytes } from '@polkadot/util';
|
||||
|
||||
import { decodeAddress } from '../address/decode';
|
||||
import { naclVerify } from '../nacl/verify';
|
||||
@@ -12,13 +12,15 @@ import { schnorrkelVerify } from '../schnorrkel/verify';
|
||||
import { secp256k1Verify } from '../secp256k1/verify';
|
||||
|
||||
interface VerifyInput {
|
||||
message: Uint8Array | string;
|
||||
message: Uint8Array;
|
||||
publicKey: Uint8Array;
|
||||
signature: Uint8Array;
|
||||
}
|
||||
|
||||
type Verifier = [KeypairType, (message: Uint8Array | string, signature: Uint8Array, publicKey: Uint8Array) => boolean];
|
||||
|
||||
type VerifyFn = (result: VerifyResult, input: VerifyInput) => VerifyResult;
|
||||
|
||||
const secp256k1VerifyHasher = (hashType: 'blake2' | 'keccak') =>
|
||||
(message: Uint8Array | string, signature: Uint8Array, publicKey: Uint8Array) =>
|
||||
secp256k1Verify(message, signature, publicKey, hashType);
|
||||
@@ -75,16 +77,32 @@ function verifyMultisig (result: VerifyResult, { message, publicKey, signature }
|
||||
return result;
|
||||
}
|
||||
|
||||
function getVerifyFn (signature: Uint8Array): VerifyFn {
|
||||
return [0, 1, 2].includes(signature[0]) && [65, 66].includes(signature.length)
|
||||
? verifyMultisig
|
||||
: verifyDetect;
|
||||
}
|
||||
|
||||
export function signatureVerify (message: HexString | Uint8Array | string, signature: HexString | Uint8Array | string, addressOrPublicKey: HexString | Uint8Array | string): VerifyResult {
|
||||
const signatureU8a = u8aToU8a(signature);
|
||||
|
||||
assert([64, 65, 66].includes(signatureU8a.length), () => `Invalid signature length, expected [64..66] bytes, found ${signatureU8a.length}`);
|
||||
|
||||
const publicKey = decodeAddress(addressOrPublicKey);
|
||||
const input = { message, publicKey, signature: signatureU8a };
|
||||
const result: VerifyResult = { crypto: 'none', isValid: false, publicKey };
|
||||
const input = { message: u8aToU8a(message), publicKey, signature: signatureU8a };
|
||||
const result: VerifyResult = { crypto: 'none', isValid: false, isWrapped: u8aIsWrapped(input.message, true), publicKey };
|
||||
const isWrappedBytes = u8aIsWrapped(input.message, false);
|
||||
const verifyFn = getVerifyFn(signatureU8a);
|
||||
|
||||
return [0, 1, 2].includes(signatureU8a[0]) && [65, 66].includes(signatureU8a.length)
|
||||
? verifyMultisig(result, input)
|
||||
: verifyDetect(result, input);
|
||||
verifyFn(result, input);
|
||||
|
||||
if (result.crypto !== 'none' || (result.isWrapped && !isWrappedBytes)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
input.message = isWrappedBytes
|
||||
? u8aUnwrapBytes(input.message)
|
||||
: u8aWrapBytes(input.message);
|
||||
|
||||
return verifyFn(result, input);
|
||||
}
|
||||
|
||||
@@ -19,5 +19,6 @@ export type KeypairType = 'ed25519' | 'sr25519' | 'ecdsa' | 'ethereum';
|
||||
export interface VerifyResult {
|
||||
crypto: 'none' | KeypairType;
|
||||
isValid: boolean;
|
||||
isWrapped: boolean;
|
||||
publicKey: Uint8Array;
|
||||
}
|
||||
|
||||
@@ -16,3 +16,4 @@ export { u8aToBuffer } from './toBuffer';
|
||||
export { u8aToHex } from './toHex';
|
||||
export { u8aToString } from './toString';
|
||||
export { u8aToU8a } from './toU8a';
|
||||
export { U8A_WRAP_ETHEREUM, U8A_WRAP_POSTFIX, U8A_WRAP_PREFIX, u8aIsWrapped, u8aWrapBytes, u8aUnwrapBytes } from './wrap';
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
// Copyright 2017-2021 @polkadot/util authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Originally from https://github.com/polkadot-js/extension/pull/743
|
||||
|
||||
import { u8aConcat, u8aEq, u8aToString } from '@polkadot/util';
|
||||
|
||||
import { U8A_WRAP_ETHEREUM, U8A_WRAP_POSTFIX, U8A_WRAP_PREFIX, u8aUnwrapBytes, u8aWrapBytes } from './wrap';
|
||||
|
||||
const TEST_DATA = 'this is just some random message that we expect to be wrapped along the way';
|
||||
const TEST_ETH = u8aConcat(U8A_WRAP_ETHEREUM, TEST_DATA);
|
||||
const TEST_WRAP_EMPTY = `${u8aToString(U8A_WRAP_PREFIX)}${u8aToString(U8A_WRAP_POSTFIX)}`;
|
||||
const TEST_WRAP_FULL = `${u8aToString(U8A_WRAP_PREFIX)}${TEST_DATA}${u8aToString(U8A_WRAP_POSTFIX)}`;
|
||||
const TEST_WARP_HALF_PRE = `${u8aToString(U8A_WRAP_PREFIX)}${TEST_DATA}`;
|
||||
const TEST_WRAP_HALF_POST = `${TEST_DATA}${u8aToString(U8A_WRAP_POSTFIX)}`;
|
||||
|
||||
describe('u8aWrapBytes', (): void => {
|
||||
it('wraps empty bytes', (): void => {
|
||||
expect(
|
||||
u8aEq(
|
||||
u8aWrapBytes(new Uint8Array()),
|
||||
u8aConcat(U8A_WRAP_PREFIX, U8A_WRAP_POSTFIX)
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('wraps when no wrapping is detected', (): void => {
|
||||
expect(
|
||||
u8aToString(
|
||||
u8aWrapBytes(TEST_DATA)
|
||||
)
|
||||
).toEqual(TEST_WRAP_FULL);
|
||||
});
|
||||
|
||||
it('wraps when only start wrap is detected', (): void => {
|
||||
expect(
|
||||
u8aToString(
|
||||
u8aWrapBytes(TEST_WARP_HALF_PRE)
|
||||
)
|
||||
).toEqual(`${u8aToString(U8A_WRAP_PREFIX)}${TEST_WARP_HALF_PRE}${u8aToString(U8A_WRAP_POSTFIX)}`);
|
||||
});
|
||||
|
||||
it('wraps when only end wrap is detected', (): void => {
|
||||
expect(
|
||||
u8aToString(
|
||||
u8aWrapBytes(TEST_WRAP_HALF_POST)
|
||||
)
|
||||
).toEqual(`${u8aToString(U8A_WRAP_PREFIX)}${TEST_WRAP_HALF_POST}${u8aToString(U8A_WRAP_POSTFIX)}`);
|
||||
});
|
||||
|
||||
it('does not re-wrap when a wrap is already present', (): void => {
|
||||
expect(
|
||||
u8aToString(
|
||||
u8aWrapBytes(TEST_WRAP_FULL)
|
||||
)
|
||||
).toEqual(TEST_WRAP_FULL);
|
||||
});
|
||||
|
||||
it('does not re-wrap when a wrap (empty data) is already present', (): void => {
|
||||
expect(
|
||||
u8aToString(
|
||||
u8aWrapBytes(TEST_WRAP_EMPTY)
|
||||
)
|
||||
).toEqual(TEST_WRAP_EMPTY);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unwrapBytes', (): void => {
|
||||
it('unwraps empty bytes', (): void => {
|
||||
expect(
|
||||
u8aEq(
|
||||
u8aUnwrapBytes(new Uint8Array()),
|
||||
new Uint8Array()
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('unwraps when no wrapping is detected', (): void => {
|
||||
expect(
|
||||
u8aToString(
|
||||
u8aUnwrapBytes(TEST_DATA)
|
||||
)
|
||||
).toEqual(TEST_DATA);
|
||||
});
|
||||
|
||||
it('unwraps when no wrapping is detected (only start)', (): void => {
|
||||
expect(
|
||||
u8aToString(
|
||||
u8aUnwrapBytes(TEST_WARP_HALF_PRE)
|
||||
)
|
||||
).toEqual(TEST_WARP_HALF_PRE);
|
||||
});
|
||||
|
||||
it('unwraps when no wrapping is detected (only end)', (): void => {
|
||||
expect(
|
||||
u8aToString(
|
||||
u8aUnwrapBytes(TEST_WRAP_HALF_POST)
|
||||
)
|
||||
).toEqual(TEST_WRAP_HALF_POST);
|
||||
});
|
||||
|
||||
it('unwraps when a wrap is present', (): void => {
|
||||
expect(
|
||||
u8aToString(
|
||||
u8aUnwrapBytes(TEST_WRAP_FULL)
|
||||
)
|
||||
).toEqual(TEST_DATA);
|
||||
});
|
||||
|
||||
it('unwraps when a an empty wrap is present', (): void => {
|
||||
expect(
|
||||
u8aToString(
|
||||
u8aUnwrapBytes(TEST_WRAP_EMPTY)
|
||||
)
|
||||
).toEqual('');
|
||||
});
|
||||
|
||||
describe('Ethereum-style', (): void => {
|
||||
it('does not wrap an Ethereum wrap', (): void => {
|
||||
expect(
|
||||
u8aEq(
|
||||
u8aWrapBytes(TEST_ETH),
|
||||
TEST_ETH
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('does not unwrap an Ethereum wrap', (): void => {
|
||||
expect(
|
||||
u8aEq(
|
||||
u8aUnwrapBytes(TEST_ETH),
|
||||
TEST_ETH
|
||||
)
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright 2017-2021 @polkadot/util authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Originally from https://github.com/polkadot-js/extension/pull/743
|
||||
|
||||
import type { U8aLike } from '../types';
|
||||
|
||||
import { u8aConcat } from './concat';
|
||||
import { u8aEq } from './eq';
|
||||
import { u8aToU8a } from './toU8a';
|
||||
|
||||
export const U8A_WRAP_ETHEREUM = u8aToU8a('\x19Ethereum Signed Message:\n');
|
||||
export const U8A_WRAP_PREFIX = u8aToU8a('<Bytes>');
|
||||
export const U8A_WRAP_POSTFIX = u8aToU8a('</Bytes>');
|
||||
|
||||
const WRAP_LEN = U8A_WRAP_PREFIX.length + U8A_WRAP_POSTFIX.length;
|
||||
|
||||
export function u8aIsWrapped (u8a: Uint8Array, withEthereum: boolean): boolean {
|
||||
return (
|
||||
(
|
||||
u8a.length >= WRAP_LEN &&
|
||||
u8aEq(u8a.subarray(0, U8A_WRAP_PREFIX.length), U8A_WRAP_PREFIX) &&
|
||||
u8aEq(u8a.slice(-U8A_WRAP_POSTFIX.length), U8A_WRAP_POSTFIX)
|
||||
) ||
|
||||
(
|
||||
withEthereum &&
|
||||
u8a.length >= U8A_WRAP_ETHEREUM.length &&
|
||||
u8aEq(u8a.subarray(0, U8A_WRAP_ETHEREUM.length), U8A_WRAP_ETHEREUM)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function u8aUnwrapBytes (bytes: U8aLike): Uint8Array {
|
||||
const u8a = u8aToU8a(bytes);
|
||||
|
||||
// we don't want to unwrap Ethereum-style wraps
|
||||
return u8aIsWrapped(u8a, false)
|
||||
? u8a.subarray(U8A_WRAP_PREFIX.length, u8a.length - U8A_WRAP_POSTFIX.length)
|
||||
: u8a;
|
||||
}
|
||||
|
||||
export function u8aWrapBytes (bytes: U8aLike): Uint8Array {
|
||||
const u8a = u8aToU8a(bytes);
|
||||
|
||||
// if Ethereum-wrapping, we don't add our wrapping bytes
|
||||
return u8aIsWrapped(u8a, true)
|
||||
? u8a
|
||||
: u8aConcat(U8A_WRAP_PREFIX, u8a, U8A_WRAP_POSTFIX);
|
||||
}
|
||||
Reference in New Issue
Block a user