Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ed25519 keys support and import keys new flow #126

Merged
merged 26 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from 17 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
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"editor.codeActionsOnSave": {
"source.fixAll": true
"source.fixAll": "explicit"
},
"cSpell.words": ["Mutex", "Mutexes", "toruslabs"]
}
6 changes: 5 additions & 1 deletion src/Point.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ class Point {
encode(enc: string): Buffer {
switch (enc) {
case "arr":
return Buffer.concat([Buffer.from("04", "hex"), Buffer.from(this.x.toString("hex"), "hex"), Buffer.from(this.y.toString("hex"), "hex")]);
return Buffer.concat([
Buffer.from("04", "hex"),
Buffer.from(this.x.toString("hex", 64), "hex"),
Buffer.from(this.y.toString("hex", 64), "hex"),
]);
case "elliptic-compressed": {
const key = this.ecCurve.keyFromPublic({ x: this.x.toString("hex", 64), y: this.y.toString("hex", 64) }, "hex");
return Buffer.from(key.getPublic(true, "hex"));
Expand Down
4 changes: 2 additions & 2 deletions src/Share.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ class Share {

toJSON(): StringifiedType {
return {
share: this.share.toString("hex"),
shareIndex: this.shareIndex.toString("hex"),
share: this.share.toString("hex", 64),
shareIndex: this.shareIndex.toString("hex", 64),
};
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export const JRPC_METHODS = {
GET_OR_SET_KEY: "GetPubKeyOrKeyAssign",
COMMITMENT_REQUEST: "CommitmentRequest",
IMPORT_SHARE: "ImportShare",
IMPORT_SHARES: "ImportShares",
GET_SHARE_OR_KEY_ASSIGN: "GetShareOrKeyAssign",
};
24 changes: 17 additions & 7 deletions src/helpers/common.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Ecies } from "@toruslabs/eccrypto";
import { BN } from "bn.js";
import JsonStringify from "json-stable-stringify";

import { EciesHex, VerifierLookupResponse } from "../interfaces";
import { keccak256 } from ".";

// this function normalizes the result from nodes before passing the result to threshold check function
// For ex: some fields returns by nodes might be different from each other
Expand All @@ -13,13 +15,14 @@ export const normalizeKeysResult = (result: VerifierLookupResponse) => {
is_new_key: result.is_new_key,
};
if (result && result.keys && result.keys.length > 0) {
finalResult.keys = result.keys.map((key) => {
return {
pub_key_X: key.pub_key_X,
pub_key_Y: key.pub_key_Y,
address: key.address,
};
});
const finalKey = result.keys[0];
Copy link
Member

Choose a reason for hiding this comment

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

why are we using only the first key?

Copy link
Member Author

Choose a reason for hiding this comment

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

So there are cases where it is possible that a user can have multiple keys but all the nodes might not have all those shares/pub keys for ex i have seen these cases when a node is temp down but key was assigned successfully by other threshold number of nodes.

and as a result we should only check threshold for first key rather than checking older keys which are not even being used on frontend and can have mismatch in threshold when some nodes have more keys assigned for a user than others.

finalResult.keys = [
{
pub_key_X: finalKey.pub_key_X,
pub_key_Y: finalKey.pub_key_Y,
address: finalKey.address,
},
];
}
return finalResult;
};
Expand Down Expand Up @@ -83,3 +86,10 @@ export function encParamsHexToBuf(eciesData: Omit<EciesHex, "ciphertext">): Omit
mac: Buffer.from(eciesData.mac, "hex"),
};
}

export function getProxyCoordinatorEndpointIndex(endpoints: string[], verifier: string, verifierId: string) {
const verifierIdStr = `${verifier}${verifierId}`;
const hashedVerifierId = keccak256(Buffer.from(verifierIdStr, "utf8"));
const proxyEndpointNum = new BN(hashedVerifierId, "hex").mod(new BN(endpoints.length)).toNumber();
return proxyEndpointNum;
}
82 changes: 78 additions & 4 deletions src/helpers/keyUtils.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import { INodePub } from "@toruslabs/constants";
import { Ecies, encrypt } from "@toruslabs/eccrypto";
import BN from "bn.js";
import { ec } from "elliptic";
import { curve, ec as EC } from "elliptic";
import { keccak256 as keccakHash } from "ethereum-cryptography/keccak";
import stringify from "json-stable-stringify";

import { ImportedShare, KeyType } from "..";
import log from "../loglevel";
import { encParamsBufToHex, generateNonceMetadataParams, generateRandomPolynomial } from ".";

export function keccak256(a: Buffer): string {
const hash = Buffer.from(keccakHash(a)).toString("hex");
return `0x${hash}`;
}

export const generatePrivateKey = (ecCurve: EC, buf: typeof Buffer): Buffer => {
return ecCurve.genKeyPair().getPrivate().toArrayLike(buf, "le", 32);
};

export function stripHexPrefix(str: string): string {
return str.startsWith("0x") ? str.slice(2) : str;
}
Expand All @@ -31,24 +40,89 @@ export function toChecksumAddress(hexAddress: string): string {
return ret;
}

export function generateAddressFromPrivKey(ecCurve: ec, privateKey: BN): string {
export function generateAddressFromPrivKey(ecCurve: EC, privateKey: BN): string {
const key = ecCurve.keyFromPrivate(privateKey.toString("hex", 64), "hex");
const publicKey = key.getPublic().encode("hex", false).slice(2);
log.info(publicKey, "public key");
const evmAddressLower = `0x${keccak256(Buffer.from(publicKey, "hex")).slice(64 - 38)}`;
return toChecksumAddress(evmAddressLower);
}

export function generateAddressFromPubKey(ecCurve: ec, publicKeyX: BN, publicKeyY: BN): string {
export function generateAddressFromPubKey(ecCurve: EC, publicKeyX: BN, publicKeyY: BN): string {
const key = ecCurve.keyFromPublic({ x: publicKeyX.toString("hex", 64), y: publicKeyY.toString("hex", 64) });
const publicKey = key.getPublic().encode("hex", false).slice(2);
log.info(key.getPublic().encode("hex", false), "public key");
const evmAddressLower = `0x${keccak256(Buffer.from(publicKey, "hex")).slice(64 - 38)}`;
return toChecksumAddress(evmAddressLower);
}

export function getPostboxKeyFrom1OutOf1(ecCurve: ec, privKey: string, nonce: string): string {
export function getPostboxKeyFrom1OutOf1(ecCurve: EC, privKey: string, nonce: string): string {
const privKeyBN = new BN(privKey, 16);
const nonceBN = new BN(nonce, 16);
return privKeyBN.sub(nonceBN).umod(ecCurve.curve.n).toString("hex");
Copy link
Member

Choose a reason for hiding this comment

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

some users / libraries may expect the hex to be 32 bytes long (64 characters). ideally should use a global function to encode to bytes / hex, so that we don't have to do and check this manually every time. (even better might be to create a package that can be used across libraries.)

Copy link
Member Author

Choose a reason for hiding this comment

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

will address these suggestions to make test cases modular in other PRs, thanks for suggesting these improvement

}

export function derivePubKey(ecCurve: EC, sk: BN): curve.base.BasePoint {
const skHex = sk.toString(16, 64);
return ecCurve.keyFromPrivate(skHex).getPublic();
}

export const encryptionEC = new EC("secp256k1");

export const generateShares = async (
ecCurve: EC,
keyType: KeyType,
serverTimeOffset: number,
nodeIndexes: number[],
nodePubkeys: INodePub[],
privKey: string
) => {
const key = ecCurve.keyFromPrivate(privKey.padStart(64, "0"), "hex");

const threshold = ~~(nodePubkeys.length / 2) + 1;
const degree = threshold - 1;
const nodeIndexesBn: BN[] = [];

for (const nodeIndex of nodeIndexes) {
nodeIndexesBn.push(new BN(nodeIndex));
}
const privKeyBn = key.getPrivate();
const randomNonce = new BN(generatePrivateKey(ecCurve, Buffer));
const oAuthKey = privKeyBn.sub(randomNonce).umod(ecCurve.curve.n);
const oAuthPubKey = ecCurve.keyFromPrivate(oAuthKey.toString("hex").padStart(64, "0")).getPublic();
const poly = generateRandomPolynomial(ecCurve, degree, oAuthKey);
const shares = poly.generateShares(nodeIndexesBn);
const nonceParams = generateNonceMetadataParams(ecCurve, serverTimeOffset, "getOrSetNonce", oAuthKey, keyType, randomNonce);
const nonceData = Buffer.from(stringify(nonceParams.set_data), "utf8").toString("base64");
const sharesData: ImportedShare[] = [];
const encPromises: Promise<Ecies>[] = [];
for (let i = 0; i < nodeIndexesBn.length; i++) {
const shareJson = shares[nodeIndexesBn[i].toString("hex", 64)].toJSON() as Record<string, string>;
if (!nodePubkeys[i]) {
throw new Error(`Missing node pub key for node index: ${nodeIndexesBn[i].toString("hex", 64)}`);
}
const nodePubKey = encryptionEC.keyFromPublic({ x: nodePubkeys[i].X, y: nodePubkeys[i].Y });
encPromises.push(
encrypt(Buffer.from(nodePubKey.getPublic().encodeCompressed("hex"), "hex"), Buffer.from(shareJson.share.padStart(64, "0"), "hex"))
);
}
const encShares = await Promise.all(encPromises);
for (let i = 0; i < nodeIndexesBn.length; i++) {
const shareJson = shares[nodeIndexesBn[i].toString("hex", 64)].toJSON() as Record<string, string>;
const encParams = encShares[i];
const encParamsMetadata = encParamsBufToHex(encParams);
const shareData: ImportedShare = {
pub_key_x: oAuthPubKey.getX().toString("hex", 64),
pub_key_y: oAuthPubKey.getY().toString("hex", 64),
encrypted_share: encParamsMetadata.ciphertext,
encrypted_share_metadata: encParamsMetadata,
node_index: Number.parseInt(shareJson.shareIndex, 16),
key_type: keyType,
nonce_data: nonceData,
nonce_signature: nonceParams.signature,
};
sharesData.push(shareData);
}

return sharesData;
};
18 changes: 9 additions & 9 deletions src/helpers/langrangeInterpolatePoly.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { generatePrivate } from "@toruslabs/eccrypto";
import BN from "bn.js";
import { ec as EC } from "elliptic";

import Point from "../Point";
import Polynomial from "../Polynomial";
import Share from "../Share";
import { generatePrivateKey } from ".";

function generatePrivateExcludingIndexes(shareIndexes: BN[]): BN {
const key = new BN(generatePrivate());
function generatePrivateExcludingIndexes(shareIndexes: BN[], ecCurve: EC): BN {
const key = new BN(generatePrivateKey(ecCurve, Buffer));
if (shareIndexes.find((el) => el.eq(key))) {
return generatePrivateExcludingIndexes(shareIndexes);
return generatePrivateExcludingIndexes(shareIndexes, ecCurve);
}
return key;
}
Expand Down Expand Up @@ -111,12 +111,12 @@ export function lagrangeInterpolation(ecCurve: EC, shares: BN[], nodeIndex: BN[]
export function generateRandomPolynomial(ecCurve: EC, degree: number, secret?: BN, deterministicShares?: Share[]): Polynomial {
let actualS = secret;
if (!secret) {
actualS = generatePrivateExcludingIndexes([new BN(0)]);
actualS = generatePrivateExcludingIndexes([new BN(0)], ecCurve);
}
if (!deterministicShares) {
const poly = [actualS];
for (let i = 0; i < degree; i += 1) {
const share = generatePrivateExcludingIndexes(poly);
const share = generatePrivateExcludingIndexes(poly, ecCurve);
poly.push(share);
}
return new Polynomial(poly, ecCurve);
Expand All @@ -133,11 +133,11 @@ export function generateRandomPolynomial(ecCurve: EC, degree: number, secret?: B
points[share.shareIndex.toString("hex", 64)] = new Point(share.shareIndex, share.share, ecCurve);
});
for (let i = 0; i < degree - deterministicShares.length; i += 1) {
let shareIndex = generatePrivateExcludingIndexes([new BN(0)]);
let shareIndex = generatePrivateExcludingIndexes([new BN(0)], ecCurve);
while (points[shareIndex.toString("hex", 64)] !== undefined) {
shareIndex = generatePrivateExcludingIndexes([new BN(0)]);
shareIndex = generatePrivateExcludingIndexes([new BN(0)], ecCurve);
}
points[shareIndex.toString("hex", 64)] = new Point(shareIndex, new BN(generatePrivate()), ecCurve);
points[shareIndex.toString("hex", 64)] = new Point(shareIndex, new BN(generatePrivateKey(ecCurve, Buffer)), ecCurve);
}
points["0"] = new Point(new BN(0), actualS, ecCurve);
return lagrangeInterpolatePolynomial(ecCurve, Object.values(points));
Expand Down
38 changes: 34 additions & 4 deletions src/helpers/metadataUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ec } from "elliptic";
import stringify from "json-stable-stringify";
import log from "loglevel";

import { EciesHex, GetOrSetNonceResult, MetadataParams } from "../interfaces";
import { EciesHex, GetOrSetNonceResult, KeyType, MetadataParams, NonceMetadataParams, SetNonceData } from "../interfaces";
import { encParamsHexToBuf } from "./common";
import { keccak256 } from "./keyUtils";

Expand All @@ -25,7 +25,7 @@ export async function decryptNodeData(eciesData: EciesHex, ciphertextHex: string
return decryptedSigBuffer;
}

export function generateMetadataParams(ecCurve: ec, serverTimeOffset: number, message: string, privateKey: BN): MetadataParams {
export function generateMetadataParams(ecCurve: ec, serverTimeOffset: number, message: string, privateKey: BN, keyType: KeyType): MetadataParams {
const key = ecCurve.keyFromPrivate(privateKey.toString("hex", 64));
const setData = {
data: message,
Expand All @@ -36,6 +36,7 @@ export function generateMetadataParams(ecCurve: ec, serverTimeOffset: number, me
pub_key_X: key.getPublic().getX().toString("hex"), // DO NOT PAD THIS. BACKEND DOESN'T
pub_key_Y: key.getPublic().getY().toString("hex"), // DO NOT PAD THIS. BACKEND DOESN'T
set_data: setData,
key_type: keyType,
signature: Buffer.from(sig.r.toString(16, 64) + sig.s.toString(16, 64) + new BN("").toString(16, 2), "hex").toString("base64"),
};
}
Expand All @@ -60,6 +61,7 @@ export async function getMetadata(
export async function getOrSetNonce(
legacyMetadataHost: string,
ecCurve: ec,
keyType: KeyType,
serverTimeOffset: number,
X: string,
Y: string,
Expand All @@ -69,7 +71,7 @@ export async function getOrSetNonce(
let data: Data;
const msg = getOnly ? "getNonce" : "getOrSetNonce";
if (privKey) {
data = generateMetadataParams(ecCurve, serverTimeOffset, msg, privKey);
data = generateMetadataParams(ecCurve, serverTimeOffset, msg, privKey, keyType);
} else {
data = {
pub_key_X: X,
Expand All @@ -83,10 +85,38 @@ export async function getOrSetNonce(
export async function getNonce(
legacyMetadataHost: string,
ecCurve: ec,
keyType: KeyType,
serverTimeOffset: number,
X: string,
Y: string,
privKey?: BN
): Promise<GetOrSetNonceResult> {
return getOrSetNonce(legacyMetadataHost, ecCurve, serverTimeOffset, X, Y, privKey, true);
return getOrSetNonce(legacyMetadataHost, ecCurve, keyType, serverTimeOffset, X, Y, privKey, true);
}

export function generateNonceMetadataParams(
ecCurve: ec,
serverTimeOffset: number,
operation: string,
privateKey: BN,
keyType: KeyType,
nonce?: BN
): NonceMetadataParams {
const key = ecCurve.keyFromPrivate(privateKey.toString("hex", 64));
const setData: Partial<SetNonceData> = {
operation,
timestamp: new BN(~~(serverTimeOffset + Date.now() / 1000)).toString(16),
};

if (nonce) {
setData.data = nonce.toString("hex", 64);
}
const sig = key.sign(keccak256(Buffer.from(stringify(setData), "utf8")).slice(2));
return {
pub_key_X: key.getPublic().getX().toString("hex", 64),
pub_key_Y: key.getPublic().getY().toString("hex", 64),
set_data: setData,
key_type: keyType,
signature: Buffer.from(sig.r.toString(16, 64) + sig.s.toString(16, 64) + new BN("").toString(16, 2), "hex").toString("base64"),
};
}
Loading