diff --git a/src/lib/enr/enr.spec.ts b/src/lib/enr/enr.spec.ts index 46ad13f8bd..fdbbbb18c3 100644 --- a/src/lib/enr/enr.spec.ts +++ b/src/lib/enr/enr.spec.ts @@ -190,11 +190,11 @@ describe("ENR", function () { }); }); - describe("Static tests", () => { + describe("Static tests", function () { let privateKey: Uint8Array; let record: ENR; - beforeEach(() => { + beforeEach(async function () { const seq = BigInt(1); privateKey = hexToBytes( "b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291" @@ -202,8 +202,7 @@ describe("ENR", function () { record = ENR.createV4(v4.publicKey(privateKey)); record.setLocationMultiaddr(new Multiaddr("/ip4/127.0.0.1/udp/30303")); record.seq = seq; - // To set signature - record.encode(privateKey); + await record.encodeTxt(privateKey); }); it("should properly compute the node id", () => { @@ -212,21 +211,24 @@ describe("ENR", function () { ); }); - it("should encode/decode to RLP encoding", () => { - const decoded = ENR.decode(record.encode(privateKey)); + it("should encode/decode to RLP encoding", async function () { + const decoded = ENR.decode(await record.encode(privateKey)); expect(decoded).to.deep.equal(record); }); - it("should encode/decode to text encoding", () => { + it("should encode/decode to text encoding", async function () { // spec enr https://eips.ethereum.org/EIPS/eip-778 const testTxt = "enr:-IS4QHCYrYZbAKWCBRlAy5zzaDZXJBGkcnh4MHcBFZntXNFrdvJjX04jRzjzCBOonrkTfj499SZuOh8R33Ls8RRcy5wBgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQPKY0yuDUmstAHYpMa2_oxVtw0RW_QAdpzBQA8yWM0xOIN1ZHCCdl8"; const decoded = ENR.decodeTxt(testTxt); - expect(decoded.udp).to.be.equal(30303); - expect(decoded.ip).to.be.equal("127.0.0.1"); - expect(decoded).to.deep.equal(record); - const recordTxt = record.encodeTxt(privateKey); - expect(recordTxt).to.equal(testTxt); + // Note: Signatures are different due to the extra entropy added + // by @noble/secp256k1: + // https://github.com/paulmillr/noble-secp256k1#signmsghash-privatekey + expect(decoded.udp).to.deep.equal(record.udp); + expect(decoded.ip).to.deep.equal(record.ip); + expect(decoded.id).to.deep.equal(record.id); + expect(decoded.seq).to.equal(record.seq); + expect(decoded.get("secp256k1")).to.deep.equal(record.get("secp256k1")); }); }); diff --git a/src/lib/enr/enr.ts b/src/lib/enr/enr.ts index f1fa53699e..849650bad7 100644 --- a/src/lib/enr/enr.ts +++ b/src/lib/enr/enr.ts @@ -22,6 +22,7 @@ import { import { decodeMultiaddrs, encodeMultiaddrs } from "./multiaddrs_codec"; import { ENRKey, ENRValue, NodeId, SequenceNumber } from "./types"; import * as v4 from "./v4"; +import { compressPublicKey } from "./v4"; import { decodeWaku2, encodeWaku2, Waku2 } from "./waku2_codec"; const dbg = debug("waku:enr"); @@ -45,6 +46,10 @@ export class ENR extends Map { publicKey: Uint8Array, kvs: Record = {} ): ENR { + // EIP-778 specifies that the key must be in compressed format, 33 bytes + if (publicKey.length !== 33) { + publicKey = compressPublicKey(publicKey); + } return new ENR({ ...kvs, id: utf8ToBytes("v4"), @@ -453,10 +458,10 @@ export class ENR extends Map { return v4.verify(this.publicKey, data, signature); } - sign(data: Uint8Array, privateKey: Uint8Array): Uint8Array { + async sign(data: Uint8Array, privateKey: Uint8Array): Promise { switch (this.id) { case "v4": - this.signature = v4.sign(privateKey, data); + this.signature = await v4.sign(privateKey, data); break; default: throw new Error(ERR_INVALID_ID); @@ -464,7 +469,9 @@ export class ENR extends Map { return this.signature; } - encodeToValues(privateKey?: Uint8Array): (ENRKey | ENRValue | number[])[] { + async encodeToValues( + privateKey?: Uint8Array + ): Promise<(ENRKey | ENRValue | number[])[]> { // sort keys and flatten into [k, v, k, v, ...] const content: Array = Array.from(this.keys()) .sort((a, b) => a.localeCompare(b)) @@ -473,7 +480,9 @@ export class ENR extends Map { .flat(); content.unshift(new Uint8Array([Number(this.seq)])); if (privateKey) { - content.unshift(this.sign(hexToBytes(RLP.encode(content)), privateKey)); + content.unshift( + await this.sign(hexToBytes(RLP.encode(content)), privateKey) + ); } else { if (!this.signature) { throw new Error(ERR_NO_SIGNATURE); @@ -483,8 +492,10 @@ export class ENR extends Map { return content; } - encode(privateKey?: Uint8Array): Uint8Array { - const encoded = hexToBytes(RLP.encode(this.encodeToValues(privateKey))); + async encode(privateKey?: Uint8Array): Promise { + const encoded = hexToBytes( + RLP.encode(await this.encodeToValues(privateKey)) + ); if (encoded.length >= MAX_RECORD_SIZE) { throw new Error("ENR must be less than 300 bytes"); } @@ -492,6 +503,8 @@ export class ENR extends Map { } encodeTxt(privateKey?: Uint8Array): string { - return ENR.RECORD_PREFIX + toString(this.encode(privateKey), "base64url"); + return ( + ENR.RECORD_PREFIX + toString(await this.encode(privateKey), "base64url") + ); } } diff --git a/src/lib/enr/v4.ts b/src/lib/enr/v4.ts index 0e70dc2640..ad6b7920cc 100644 --- a/src/lib/enr/v4.ts +++ b/src/lib/enr/v4.ts @@ -1,7 +1,8 @@ +import * as secp from "@noble/secp256k1"; import { keccak256 } from "js-sha3"; -import * as secp256k1 from "secp256k1"; import { randomBytes } from "../crypto"; +import { bytesToHex } from "../utils"; import { createNodeId } from "./create"; import { NodeId } from "./types"; @@ -15,12 +16,21 @@ export function createPrivateKey(): Uint8Array { } export function publicKey(privKey: Uint8Array): Uint8Array { - return secp256k1.publicKeyCreate(privKey); + return secp.getPublicKey(privKey, true); } -export function sign(privKey: Uint8Array, msg: Uint8Array): Uint8Array { - const { signature } = secp256k1.ecdsaSign(hash(msg), privKey); - return signature; +export function compressPublicKey(publicKey: Uint8Array): Uint8Array { + const point = secp.Point.fromHex(bytesToHex(publicKey)); + return point.toRawBytes(true); +} + +export async function sign( + privKey: Uint8Array, + msg: Uint8Array +): Promise { + return secp.sign(hash(msg), privKey, { + der: false, + }); } export function verify( @@ -28,12 +38,17 @@ export function verify( msg: Uint8Array, sig: Uint8Array ): boolean { - // Remove the recovery id if present (byte #65) - return secp256k1.ecdsaVerify(sig.slice(0, 64), hash(msg), pubKey); + try { + const _sig = secp.Signature.fromCompact(sig.slice(0, 64)); + return secp.verify(_sig, hash(msg), pubKey); + } catch { + return false; + } } export function nodeId(pubKey: Uint8Array): NodeId { - const uncompressedPubkey = secp256k1.publicKeyConvert(pubKey, false); + const publicKey = secp.Point.fromHex(pubKey); + const uncompressedPubkey = publicKey.toRawBytes(false); return createNodeId(hash(uncompressedPubkey.slice(1))); } @@ -47,7 +62,7 @@ export class ENRKeyPair { public static create(privateKey?: Uint8Array): ENRKeyPair { if (privateKey) { - if (!secp256k1.privateKeyVerify(privateKey)) { + if (!secp.utils.isValidPrivateKey(privateKey)) { throw new Error("Invalid private key"); } } @@ -58,7 +73,7 @@ export class ENRKeyPair { return new ENRKeyPair(_nodeId, _privateKey, _publicKey); } - public sign(msg: Uint8Array): Uint8Array { + public async sign(msg: Uint8Array): Promise { return sign(this.privateKey, msg); }