Ethereum keypair signing & verification (#664)
* Ethereum keypair signing & verification * Verify against hashes expanded * Adjust via flags * ss58 encoding for 33-byte publicKey * ecdsa types stores address as raw publicKey * Adjust for hash lengths * Adjust expand/compress specs
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
import sshash from './sshash';
|
||||
|
||||
export default function checkChecksum (decoded: Uint8Array): [boolean, number] {
|
||||
const isPublicKey = decoded.length === 35;
|
||||
const isPublicKey = [35, 36].includes(decoded.length);
|
||||
|
||||
// non-publicKeys has 1 byte checksums, else default to 2
|
||||
const length = decoded.length - (isPublicKey ? 2 : 1);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { stringToU8a, u8aToHex } from '@polkadot/util';
|
||||
import { stringToU8a, u8aToHex, hexToU8a } from '@polkadot/util';
|
||||
|
||||
import testingPairs, { TestKeyringMap } from '../../../keyring/src/testingPairs';
|
||||
import decode from './decode';
|
||||
@@ -74,6 +74,14 @@ describe('decodeAddress', (): void => {
|
||||
).toEqual(new Uint8Array([42, 44, 10, 0, 0, 0, 0, 0]));
|
||||
});
|
||||
|
||||
it('decodes a 33-byte address', (): void => {
|
||||
expect(
|
||||
decode('KWCv1L3QX9LDPwY4VzvLmarEmXjVJidUzZcinvVnmxAJJCBou')
|
||||
).toEqual(
|
||||
hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077')
|
||||
);
|
||||
});
|
||||
|
||||
it.skip('allows invalid prefix (in list)', (): void => {
|
||||
expect(
|
||||
(): Uint8Array => decode('6GfvWUvHvU8otbZ7sFhXH4eYeMcKdUkL61P3nFy52efEPVUx')
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
import { allowedSS58 } from './ss58';
|
||||
|
||||
const defaults = {
|
||||
allowedDecodedLengths: [1, 2, 4, 8, 32],
|
||||
allowedDecodedLengths: [1, 2, 4, 8, 32, 33],
|
||||
// publicKey has prefix + 2 checksum bytes, short only prefix + 1 checksum byte
|
||||
allowedEncodedLengths: [3, 4, 6, 10, 35],
|
||||
allowedEncodedLengths: [3, 4, 6, 10, 35, 36],
|
||||
allowedPrefix: allowedSS58,
|
||||
prefix: 42
|
||||
};
|
||||
|
||||
@@ -67,11 +67,17 @@ describe('encode', (): void => {
|
||||
).toEqual('as7QnGMf');
|
||||
});
|
||||
|
||||
it('enodes a 8-byte address', (): void => {
|
||||
it('encodes a 8-byte address', (): void => {
|
||||
expect(
|
||||
encode(
|
||||
new Uint8Array([42, 44, 10, 0, 0, 0, 0, 0]), 68
|
||||
)
|
||||
).toEqual('4q7qY5RBG7Z4wv');
|
||||
});
|
||||
|
||||
it('encodes an 33-byte address', (): void => {
|
||||
expect(
|
||||
encode('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077')
|
||||
).toEqual('KWCv1L3QX9LDPwY4VzvLmarEmXjVJidUzZcinvVnmxAJJCBou');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function encode (_key: Uint8Array | string, ss58Format: Prefix =
|
||||
|
||||
assert(defaults.allowedDecodedLengths.includes(key.length), `Expected a valid key to convert, with length ${defaults.allowedDecodedLengths.join(', ')}`);
|
||||
|
||||
const isPublicKey = key.length === 32;
|
||||
const isPublicKey = [32, 33].includes(key.length);
|
||||
const input = u8aConcat(new Uint8Array([ss58Format]), key);
|
||||
const hash = sshash(input);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { keccakAsU8a } from '../keccak';
|
||||
import { ethereumEncode } from './';
|
||||
|
||||
describe('formatAddress', () => {
|
||||
@@ -35,12 +36,6 @@ describe('formatAddress', () => {
|
||||
const ADDRESS = '0x4119b2e6c3Cb618F4f0B93ac77f9BeeC7FF02887';
|
||||
|
||||
it('encodes a compressed publicKey', (): void => {
|
||||
expect(
|
||||
ethereumEncode('0xb9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077')
|
||||
).toEqual(ADDRESS);
|
||||
});
|
||||
|
||||
it('encodes a compressed publicKey (indicator)', (): void => {
|
||||
expect(
|
||||
ethereumEncode('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077')
|
||||
).toEqual(ADDRESS);
|
||||
@@ -48,13 +43,15 @@ describe('formatAddress', () => {
|
||||
|
||||
it('encodes an expanded publicKey', (): void => {
|
||||
expect(
|
||||
ethereumEncode('0xb9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af')
|
||||
ethereumEncode('0x04b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af')
|
||||
).toEqual(ADDRESS);
|
||||
});
|
||||
|
||||
it('encodes an expanded publicKey (indicator + x/y)', (): void => {
|
||||
it('encodes a pre-hashed key', (): void => {
|
||||
expect(
|
||||
ethereumEncode('0x04b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af')
|
||||
ethereumEncode(
|
||||
keccakAsU8a('0xb9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af')
|
||||
)
|
||||
).toEqual(ADDRESS);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,20 +7,24 @@ import { assert, u8aToHex, u8aToU8a } from '@polkadot/util';
|
||||
import { keccakAsU8a } from '../keccak';
|
||||
import { secp256k1Expand } from '../secp256k1';
|
||||
|
||||
function getH160 (u8a: Uint8Array): Uint8Array {
|
||||
if ([33, 65].includes(u8a.length)) {
|
||||
u8a = keccakAsU8a(secp256k1Expand(u8a));
|
||||
}
|
||||
|
||||
return u8a.slice(-20);
|
||||
}
|
||||
|
||||
export default function ethereumEncode (addressOrPublic?: string | Uint8Array): string {
|
||||
if (!addressOrPublic) {
|
||||
return '0x';
|
||||
}
|
||||
|
||||
let u8aAddress = u8aToU8a(addressOrPublic);
|
||||
const u8aAddress = u8aToU8a(addressOrPublic);
|
||||
|
||||
assert([20, 32, 33, 64, 65].includes(u8aAddress.length), 'Invalid address or publicKey passed');
|
||||
assert([20, 32, 33, 65].includes(u8aAddress.length), 'Invalid address or publicKey passed');
|
||||
|
||||
if (u8aAddress.length > 20) {
|
||||
u8aAddress = keccakAsU8a(secp256k1Expand(u8aAddress)).slice(-20);
|
||||
}
|
||||
|
||||
const address = u8aToHex(u8aAddress, -1, false);
|
||||
const address = u8aToHex(getH160(u8aAddress), -1, false);
|
||||
const hash = u8aToHex(keccakAsU8a(address), -1, false);
|
||||
let result = '';
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ describe('secp256k1Compress', (): void => {
|
||||
it('compresses a known key', (): void => {
|
||||
expect(
|
||||
secp256k1Compress(
|
||||
hexToU8a('0xb9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af')
|
||||
hexToU8a('0x04b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af')
|
||||
)
|
||||
).toEqual(
|
||||
hexToU8a('0x03b9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077')
|
||||
|
||||
@@ -3,16 +3,15 @@
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import elliptic from 'elliptic';
|
||||
|
||||
import sanitizePublic from './sanitizePublic';
|
||||
import { assert } from '@polkadot/util';
|
||||
|
||||
const EC = elliptic.ec;
|
||||
const ec = new EC('secp256k1');
|
||||
|
||||
export default function secp256k1Compress (publicKey: Uint8Array): Uint8Array {
|
||||
assert([33, 65].includes(publicKey.length), 'Invalid publicKey provided');
|
||||
|
||||
return new Uint8Array(
|
||||
ec.keyFromPublic(
|
||||
sanitizePublic(publicKey)
|
||||
).getPublic().encodeCompressed()
|
||||
ec.keyFromPublic(publicKey).getPublic().encodeCompressed()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,16 +17,6 @@ describe('secp256k1Expand', (): void => {
|
||||
);
|
||||
});
|
||||
|
||||
it('expands a known key (no indicator)', (): void => {
|
||||
expect(
|
||||
secp256k1Expand(
|
||||
hexToU8a('0xb9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb13077')
|
||||
)
|
||||
).toEqual(
|
||||
hexToU8a('0xb9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af')
|
||||
);
|
||||
});
|
||||
|
||||
it('expands a known full key', (): void => {
|
||||
expect(
|
||||
secp256k1Expand(
|
||||
@@ -36,14 +26,4 @@ describe('secp256k1Expand', (): void => {
|
||||
hexToU8a('0xb9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af')
|
||||
);
|
||||
});
|
||||
|
||||
it('expands a known full key (no indicator)', (): void => {
|
||||
expect(
|
||||
secp256k1Expand(
|
||||
hexToU8a('0xb9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af')
|
||||
)
|
||||
).toEqual(
|
||||
hexToU8a('0xb9dc646dd71118e5f7fda681ad9eca36eb3ee96f344f582fbe7b5bcdebb1307763fe926c273235fd979a134076d00fd1683cbd35868cb485d4a3a640e52184af')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import elliptic from 'elliptic';
|
||||
import { bnToU8a, u8aConcat } from '@polkadot/util';
|
||||
|
||||
import sanitizePublic from './sanitizePublic';
|
||||
import { assert, bnToU8a, u8aConcat } from '@polkadot/util';
|
||||
|
||||
const EC = elliptic.ec;
|
||||
const ec = new EC('secp256k1');
|
||||
|
||||
export default function secp256k1Expand (publicKey: Uint8Array): Uint8Array {
|
||||
const expanded = ec.keyFromPublic(sanitizePublic(publicKey)).getPublic();
|
||||
assert([33, 65].includes(publicKey.length), 'Invalid publicKey provided');
|
||||
|
||||
const expanded = ec.keyFromPublic(publicKey).getPublic();
|
||||
|
||||
return u8aConcat(
|
||||
bnToU8a(expanded.getX(), { bitLength: 256, isLe: false }),
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
// Copyright 2017-2020 @polkadot/util-crypto authors & contributors
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { assert, u8aConcat } from '@polkadot/util';
|
||||
|
||||
const INDICATOR_COMP = new Uint8Array([3]);
|
||||
const INDICATOR_FULL = new Uint8Array([4]);
|
||||
|
||||
export default function sanitizePublic (publicKey: Uint8Array): Uint8Array {
|
||||
assert([32, 33, 64, 65].includes(publicKey.length), 'Invalid publicKey provided');
|
||||
|
||||
return [33, 65].includes(publicKey.length)
|
||||
? publicKey
|
||||
: publicKey.length === 32
|
||||
? u8aConcat(INDICATOR_COMP, publicKey)
|
||||
: u8aConcat(INDICATOR_FULL, publicKey);
|
||||
}
|
||||
@@ -5,8 +5,9 @@
|
||||
import { HashType } from './types';
|
||||
|
||||
import elliptic from 'elliptic';
|
||||
import { assert, u8aToU8a } from '@polkadot/util';
|
||||
import { assert, u8aEq, u8aToU8a } from '@polkadot/util';
|
||||
|
||||
import secp256k1Expand from './expand';
|
||||
import hasher from './hasher';
|
||||
|
||||
const EC = elliptic.ec;
|
||||
@@ -16,21 +17,19 @@ const ec = new EC('secp256k1');
|
||||
* @name secp256k1Verify
|
||||
* @description Verifies the signature of `message`, using the supplied pair
|
||||
*/
|
||||
export default function secp256k1Verify (message: Uint8Array | string, signature: Uint8Array | string, address: Uint8Array | string, hashType: HashType = 'blake2'): boolean {
|
||||
const signatureU8a = u8aToU8a(signature);
|
||||
export default function secp256k1Verify (message: Uint8Array | string, signature: Uint8Array | string, address: Uint8Array | string, hashType: HashType = 'blake2', isExpanded = false): boolean {
|
||||
const u8a = u8aToU8a(signature);
|
||||
|
||||
assert(signatureU8a.length === 65, `Expected signature with 65 bytes, ${signatureU8a.length} found instead`);
|
||||
assert(u8a.length === 65, `Expected signature with 65 bytes, ${u8a.length} found instead`);
|
||||
|
||||
const sig = {
|
||||
r: signatureU8a.slice(0, 32),
|
||||
s: signatureU8a.slice(32, 64)
|
||||
};
|
||||
const recovery = signatureU8a[64];
|
||||
const publicKey = new Uint8Array(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access
|
||||
ec.recoverPubKey(hasher(hashType, message), sig, recovery)
|
||||
ec.recoverPubKey(hasher(hashType, message), { r: u8a.slice(0, 32), s: u8a.slice(32, 64) }, u8a[64])
|
||||
.encodeCompressed()
|
||||
);
|
||||
|
||||
return Buffer.compare(hasher(hashType, publicKey), u8aToU8a(address)) === 0;
|
||||
return u8aEq(
|
||||
hasher(hashType, isExpanded ? secp256k1Expand(publicKey) : publicKey),
|
||||
u8aToU8a(address)
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user