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

Secp256k1 support #15

Merged
merged 30 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
cc5a11b
feat: add support for secp256k1
gtsonevv May 7, 2024
828c84b
fix: fix linter errors
gtsonevv May 7, 2024
196291f
fix: use enum for public key data
gtsonevv May 7, 2024
2cc98d9
fix: add schema changes
gtsonevv May 7, 2024
7b178a1
fix: remove enums from schema
gtsonevv May 8, 2024
5ffc704
fix: lint
gtsonevv May 8, 2024
2bd94f9
fix: update PublicKey
gtsonevv Jun 19, 2024
ad024f1
chore: fix linter errors
gtsonevv Jun 19, 2024
15d00f3
Merge branch 'master' into add-secp256k1-support
gtsonevv Jun 19, 2024
a9051d7
Merge branch 'add-secp256k1-support' into secp256k1-support-new
gtsonevv Jun 19, 2024
a559b29
fix: revert enum changes
gtsonevv Jun 24, 2024
4f12396
fix: fix linter errors
gtsonevv Jun 24, 2024
67eb34d
fix: public key schema
gagdiez Jun 25, 2024
3991ef1
fix: update signature class and schema
gtsonevv Jun 25, 2024
c16caaa
fix: fix linter errors
gtsonevv Jun 25, 2024
c567804
fix: fix a secp256k1 test
gtsonevv Jun 25, 2024
9aa9446
fix: fix linter errors
gtsonevv Jun 25, 2024
9c3b8eb
fix: fix publickey method
gtsonevv Jun 25, 2024
7d5a824
chore: add a changeset file
gtsonevv Jun 26, 2024
effe3d4
chore: lock secp256k1 dependency version
gtsonevv Jul 3, 2024
fc951f3
test: add a secp156k1 send money test
gtsonevv Jul 4, 2024
518c361
fix: update pnpm/action-setup to v4
gtsonevv Jul 10, 2024
b79dbe5
Merge branch 'master' into secp256k1-support-new
gtsonevv Jul 10, 2024
7ce5bda
fix: fix @near-js/crypto/lib/key_pair imports
gtsonevv Jul 12, 2024
2af0435
refactor: remove duplicated Enum
gtsonevv Jul 12, 2024
65714fb
refactor: add getter to PublicKey and Signature classes
gtsonevv Jul 15, 2024
365fa88
Merge branch 'master' into secp256k1-support-new
gtsonevv Jul 15, 2024
a85175b
Merge branch 'master' into secp256k1-support-new
gtsonevv Jul 16, 2024
8c4aa1f
chore: remove duplicated test
gtsonevv Jul 22, 2024
ac393c7
Merge branch 'master' into secp256k1-support-new
gtsonevv Jul 22, 2024
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
12 changes: 12 additions & 0 deletions .changeset/wet-seals-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@near-js/crypto": minor
"@near-js/accounts": patch
"@near-js/biometric-ed25519": patch
"@near-js/keystores": patch
"near-api-js": patch
"@near-js/signers": patch
"@near-js/transactions": patch
"@near-js/wallet-account": patch
---

Add Secp256k1 support
21 changes: 21 additions & 0 deletions packages/accounts/test/account.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const { TypedError } = require('@near-js/types');
const fs = require('fs');

const { Account, Contract } = require('../lib');
const { KeyType } = require( '@near-js/crypto' );
const testUtils = require('./test-utils');

let nearjs;
Expand Down Expand Up @@ -38,6 +39,26 @@ test('create account and then view account returns the created account', async (
expect(state.amount).toEqual(newAmount.toString());
});

test('create account with a secp256k1 key and then view account returns the created account', async () => {
const newAccountName = testUtils.generateUniqueString('test');
const newAccountPublicKey = 'secp256k1:45KcWwYt6MYRnnWFSxyQVkuu9suAzxoSkUMEnFNBi9kDayTo5YPUaqMWUrf7YHUDNMMj3w75vKuvfAMgfiFXBy28';
const { amount } = await workingAccount.state();
const newAmount = BigInt(amount) / BigInt(10);
await nearjs.accountCreator.masterAccount.createAccount(newAccountName, newAccountPublicKey, newAmount);
const newAccount = new Account(nearjs.connection, newAccountName);
const state = await newAccount.state();
expect(state.amount).toEqual(newAmount.toString());
});

test('Secp256k1 send money', async() => {
const sender = await testUtils.createAccount(nearjs, KeyType.SECP256K1);
const receiver = await testUtils.createAccount(nearjs, KeyType.SECP256K1);
const { amount: receiverAmount } = await receiver.state();
await sender.sendMoney(receiver.accountId, BigInt(10000));
const state = await receiver.state();
expect(state.amount).toEqual((BigInt(receiverAmount) + BigInt(10000)).toString());
});

test('send money', async() => {
const sender = await testUtils.createAccount(nearjs);
const receiver = await testUtils.createAccount(nearjs);
Expand Down
6 changes: 3 additions & 3 deletions packages/accounts/test/test-utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { KeyPair } = require('@near-js/crypto');
const { KeyPair, KeyType } = require('@near-js/crypto');
const { InMemoryKeyStore } = require('@near-js/keystores');
const fs = require('fs').promises;
const path = require('path');
Expand Down Expand Up @@ -81,9 +81,9 @@ function generateUniqueString(prefix) {
return result + '.test.near';
}

async function createAccount({ accountCreator, connection }) {
async function createAccount({ accountCreator, connection }, keyType = KeyType.ED25519) {
const newAccountName = generateUniqueString('test');
const newPublicKey = await connection.signer.createKey(newAccountName, networkId);
const newPublicKey = await connection.signer.createKey(newAccountName, networkId, keyType);
await accountCreator.createAccount(newAccountName, newPublicKey);
return new Account(connection, newAccountName);
}
Expand Down
7 changes: 4 additions & 3 deletions packages/biometric-ed25519/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from './utils';
import { Fido2 } from './fido2';
import { AssertionResponse } from './index.d';
import { KeyPairString } from '@near-js/crypto';

const CHALLENGE_TIMEOUT_MS = 90 * 1000;
const RP_NAME = 'NEAR_API_JS_WEBAUTHN';
Expand Down Expand Up @@ -86,7 +87,7 @@ export const createKey = async (username: string): Promise<KeyPair> => {
const publicKeyBytes = get64BytePublicKeyFromPEM(publicKey);
const secretKey = sha256.create().update(Buffer.from(publicKeyBytes)).digest();
const pubKey = ed25519.getPublicKey(secretKey);
return KeyPair.fromString(baseEncode(new Uint8Array(Buffer.concat([Buffer.from(secretKey), Buffer.from(pubKey)]))));
return KeyPair.fromString(baseEncode(new Uint8Array(Buffer.concat([Buffer.from(secretKey), Buffer.from(pubKey)]))) as KeyPairString);
});
};

Expand Down Expand Up @@ -129,8 +130,8 @@ export const getKeys = async (username: string): Promise<[KeyPair, KeyPair]> =>
const firstEDPublic = ed25519.getPublicKey(firstEDSecret);
const secondEDSecret = sha256.create().update(Buffer.from(correctPKs[1])).digest();
const secondEDPublic = ed25519.getPublicKey(secondEDSecret);
const firstKeyPair = KeyPair.fromString(baseEncode(new Uint8Array(Buffer.concat([Buffer.from(firstEDSecret), Buffer.from(firstEDPublic)]))));
const secondKeyPair = KeyPair.fromString(baseEncode(new Uint8Array(Buffer.concat([Buffer.from(secondEDSecret), Buffer.from(secondEDPublic)]))));
const firstKeyPair = KeyPair.fromString(baseEncode(new Uint8Array(Buffer.concat([Buffer.from(firstEDSecret), Buffer.from(firstEDPublic)]))) as KeyPairString);
const secondKeyPair = KeyPair.fromString(baseEncode(new Uint8Array(Buffer.concat([Buffer.from(secondEDSecret), Buffer.from(secondEDPublic)]))) as KeyPairString);
return [firstKeyPair, secondKeyPair];
});
};
Expand Down
5 changes: 3 additions & 2 deletions packages/crypto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
"dependencies": {
"@near-js/types": "workspace:*",
"@near-js/utils": "workspace:*",
"borsh": "1.0.0",
"@noble/curves": "1.2.0",
"randombytes": "2.1.0"
"borsh": "1.0.0",
"randombytes": "2.1.0",
"secp256k1": "5.0.0"
},
"devDependencies": {
"@types/node": "18.11.18",
Expand Down
5 changes: 4 additions & 1 deletion packages/crypto/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
/** All supported key types */
export enum KeyType {
ED25519 = 0,
SECP256K1 = 1,
}

export enum KeySize {
SECRET_KEY = 32
SECRET_KEY = 32,
ED25519_PUBLIC_KEY = 32,
SECP256k1_PUBLIC_KEY = 64,
}
3 changes: 2 additions & 1 deletion packages/crypto/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { KeyType } from './constants';
export { KeyPair } from './key_pair';
export { KeyPair, KeyPairString } from './key_pair';
export { Signature } from './key_pair_base';
export { KeyPairEd25519 } from './key_pair_ed25519';
export { KeyPairSecp256k1 } from './key_pair_secp256k1';
export { PublicKey } from './public_key';
13 changes: 8 additions & 5 deletions packages/crypto/src/key_pair.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { KeyPairBase } from './key_pair_base';
import { KeyPairEd25519 } from './key_pair_ed25519';
import { KeyPairSecp256k1 } from './key_pair_secp256k1';

export type KeyPairString = `ed25519:${string}` | `secp256k1:${string}`;

export abstract class KeyPair extends KeyPairBase {
/**
* @param curve Name of elliptical curve, case-insensitive
* @returns Random KeyPair based on the curve
*/
static fromRandom(curve: string): KeyPair {
static fromRandom(curve: 'ed25519' | 'secp256k1'): KeyPair {
switch (curve.toUpperCase()) {
case 'ED25519': return KeyPairEd25519.fromRandom();
case 'SECP256K1': return KeyPairSecp256k1.fromRandom();
default: throw new Error(`Unknown curve ${curve}`);
}
}
Expand All @@ -18,13 +22,12 @@ export abstract class KeyPair extends KeyPairBase {
* @param encodedKey The encoded key string.
* @returns {KeyPair} The key pair created from the encoded key string.
*/
static fromString(encodedKey: string): KeyPair {
static fromString(encodedKey: KeyPairString): KeyPair {
const parts = encodedKey.split(':');
if (parts.length === 1) {
return new KeyPairEd25519(parts[0]);
} else if (parts.length === 2) {
if (parts.length === 2) {
switch (parts[0].toUpperCase()) {
case 'ED25519': return new KeyPairEd25519(parts[1]);
case 'SECP256K1': return new KeyPairSecp256k1(parts[1]);
default: throw new Error(`Unknown curve: ${parts[0]}`);
}
} else {
Expand Down
3 changes: 2 additions & 1 deletion packages/crypto/src/key_pair_base.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { KeyPairString } from './key_pair';
import { PublicKey } from './public_key';

export interface Signature {
Expand All @@ -8,6 +9,6 @@ export interface Signature {
export abstract class KeyPairBase {
abstract sign(message: Uint8Array): Signature;
abstract verify(message: Uint8Array, signature: Uint8Array): boolean;
abstract toString(): string;
abstract toString(): KeyPairString;
abstract getPublicKey(): PublicKey;
}
3 changes: 2 additions & 1 deletion packages/crypto/src/key_pair_ed25519.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import randombytes from 'randombytes';
import { KeySize, KeyType } from './constants';
import { KeyPairBase, Signature } from './key_pair_base';
import { PublicKey } from './public_key';
import { KeyPairString } from './key_pair';

/**
* This class provides key pair functionality for Ed25519 curve:
Expand Down Expand Up @@ -71,7 +72,7 @@ export class KeyPairEd25519 extends KeyPairBase {
* Returns a string representation of the key pair in the format 'ed25519:[extendedSecretKey]'.
* @returns {string} The string representation of the key pair.
*/
toString(): string {
toString(): KeyPairString {
return `ed25519:${this.extendedSecretKey}`;
}

Expand Down
78 changes: 78 additions & 0 deletions packages/crypto/src/key_pair_secp256k1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { KeySize, KeyType } from './constants';
import { KeyPairBase, Signature } from './key_pair_base';
import { PublicKey } from './public_key';
import secp256k1 from 'secp256k1';
import randombytes from 'randombytes';
import { KeyPairString } from './key_pair';
import { baseDecode, baseEncode } from '@near-js/utils';
/**
* This class provides key pair functionality for secp256k1 curve:
* generating key pairs, encoding key pairs, signing and verifying.
* nearcore expects secp256k1 public keys to be 64 bytes at all times,
* even when string encoded the secp256k1 library returns 65 byte keys
* (including a 1 byte header that indicates how the pubkey was encoded).
* We'll force the secp256k1 library to always encode uncompressed
* keys with the corresponding 0x04 header byte, then manually
* insert/remove that byte as needed.
*/
export class KeyPairSecp256k1 extends KeyPairBase {
readonly publicKey: PublicKey;
readonly secretKey: string;
readonly extendedSecretKey: string;

/**
* Construct an instance of key pair given a secret key.
* It's generally assumed that these are encoded in base58.
* @param {string} extendedSecretKey
*/
constructor(extendedSecretKey: string) {
super();
const decoded = baseDecode(extendedSecretKey);
const secretKey = new Uint8Array(decoded.slice(0, KeySize.SECRET_KEY));
const withHeader = secp256k1.publicKeyCreate(new Uint8Array(secretKey), false);
const data = withHeader.subarray(1, withHeader.length); // remove the 0x04 header byte
this.publicKey = new PublicKey({
keyType: KeyType.SECP256K1,
data
});
this.secretKey = baseEncode(secretKey);
this.extendedSecretKey = extendedSecretKey;
}

/**
* Generate a new random keypair.
* @example
* const keyRandom = KeyPair.fromRandom();
* keyRandom.publicKey
* // returns [PUBLIC_KEY]
*
* keyRandom.secretKey
* // returns [SECRET_KEY]
*/
static fromRandom() {
// TODO: find better way to generate PK
const secretKey = randombytes(KeySize.SECRET_KEY);
const withHeader = secp256k1.publicKeyCreate(new Uint8Array(secretKey), false);
const publicKey = withHeader.subarray(1, withHeader.length);
const extendedSecretKey = new Uint8Array([...secretKey, ...publicKey]);
return new KeyPairSecp256k1(baseEncode(extendedSecretKey));
}

sign(message: Uint8Array): Signature {
// nearcore expects 65 byte signatures formed by appending the recovery id to the 64 byte signature
const { signature, recid } = secp256k1.ecdsaSign(message, baseDecode(this.secretKey));
return { signature: new Uint8Array([...signature, recid]), publicKey: this.publicKey };
}

verify(message: Uint8Array, signature: Uint8Array): boolean {
return this.publicKey.verify(message, signature);
}

toString(): KeyPairString {
return `secp256k1:${this.extendedSecretKey}`;
}

getPublicKey(): PublicKey {
return this.publicKey;
}
}
58 changes: 48 additions & 10 deletions packages/crypto/src/public_key.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,44 @@
import { Assignable } from '@near-js/types';
import { baseEncode, baseDecode } from '@near-js/utils';
import { ed25519 } from '@noble/curves/ed25519';
import secp256k1 from 'secp256k1';

import { KeySize, KeyType } from './constants';
import { Assignable } from '@near-js/types';

function key_type_to_str(keyType: KeyType): string {
switch (keyType) {
case KeyType.ED25519: return 'ed25519';
case KeyType.SECP256K1: return 'secp256k1';
default: throw new Error(`Unknown key type ${keyType}`);
}
}

function str_to_key_type(keyType: string): KeyType {
switch (keyType.toLowerCase()) {
case 'ed25519': return KeyType.ED25519;
case 'secp256k1': return KeyType.SECP256K1;
default: throw new Error(`Unknown key type ${keyType}`);
}
}

class ED25519PublicKey extends Assignable { keyType: KeyType = KeyType.ED25519; data: Uint8Array; }
class SECP256K1PublicKey extends Assignable { keyType: KeyType = KeyType.SECP256K1; data: Uint8Array; }

/**
* PublicKey representation that has type and bytes of the key.
*/
export class PublicKey extends Assignable {
keyType: KeyType;
data: Uint8Array;
ed25519Key?: ED25519PublicKey;
secp256k1Key?: SECP256K1PublicKey;

constructor({ keyType, data }: { keyType: KeyType, data: Uint8Array }) {
super({});
if (keyType === KeyType.ED25519) {
this.ed25519Key = { keyType, data };
} else if (keyType === KeyType.SECP256K1) {
this.secp256k1Key = { keyType, data };
}
}

/**
* Creates a PublicKey instance from a string or an existing PublicKey instance.
Expand All @@ -45,7 +60,7 @@ export class PublicKey extends Assignable {
static fromString(encodedKey: string): PublicKey {
const parts = encodedKey.split(':');
let publicKey: string;
let keyType = KeyType.ED25519;
let keyType;
if (parts.length === 1) {
publicKey = parts[0];
} else if (parts.length === 2) {
Expand All @@ -55,8 +70,12 @@ export class PublicKey extends Assignable {
throw new Error('Invalid encoded key format, must be <curve>:<encoded key>');
}
const decodedPublicKey = baseDecode(publicKey);
if(decodedPublicKey.length !== KeySize.SECRET_KEY) {
throw new Error(`Invalid public key size (${decodedPublicKey.length}), must be ${KeySize.SECRET_KEY}`);
if (!keyType) {
keyType = decodedPublicKey.length === KeySize.SECP256k1_PUBLIC_KEY ? KeyType.SECP256K1 : KeyType.ED25519;
}
const keySize = keyType === KeyType.ED25519 ? KeySize.ED25519_PUBLIC_KEY : KeySize.SECP256k1_PUBLIC_KEY;
if (decodedPublicKey.length !== keySize) {
throw new Error(`Invalid public key size (${decodedPublicKey.length}), must be ${keySize}`);
}
return new PublicKey({ keyType, data: decodedPublicKey });
}
Expand All @@ -66,7 +85,8 @@ export class PublicKey extends Assignable {
* @returns {string} The string representation of the public key.
*/
toString(): string {
return `${key_type_to_str(this.keyType)}:${baseEncode(this.data)}`;
const encodedKey = baseEncode(this.data);
return `${key_type_to_str(this.keyType)}:${encodedKey}`;
}

/**
Expand All @@ -76,9 +96,27 @@ export class PublicKey extends Assignable {
* @returns {boolean} `true` if the signature is valid, otherwise `false`.
*/
verify(message: Uint8Array, signature: Uint8Array): boolean {
switch (this.keyType) {
case KeyType.ED25519: return ed25519.verify(signature, message, this.data);
default: throw new Error(`Unknown key type ${this.keyType}`);
const keyType = this.keyType;
const data = this.data;
switch (keyType) {
case KeyType.ED25519:
return ed25519.verify(signature, message, data);
case KeyType.SECP256K1:
return secp256k1.ecdsaVerify(signature.subarray(0, 64), message, new Uint8Array([0x04, ...data]));
default:
throw new Error(`Unknown key type: ${keyType}`);
}
}

get keyPair() {
return this.ed25519Key || this.secp256k1Key;
}

get keyType(): KeyType {
return this.keyPair.keyType;
}

get data(): Uint8Array {
return this.keyPair.data;
}
}
Loading
Loading