From 4546dea13e5145e2d061779f4eb6c39c28f0d546 Mon Sep 17 00:00:00 2001 From: oXtxNt9U <120286271+oXtxNt9U@users.noreply.github.com> Date: Tue, 1 Oct 2024 15:56:54 +0900 Subject: [PATCH] feat: restore `crypto-signature-ecdsa` package (#720) * add back crypto-signature-ecdsa * signature recovery test * fix --- .github/workflows/unit.yml | 2 + packages/crypto-signature-ecdsa/README.md | 19 ++++++ packages/crypto-signature-ecdsa/package.json | 37 +++++++++++ .../crypto-signature-ecdsa/source/index.ts | 30 +++++++++ .../source/signature.test.ts | 50 +++++++++++++++ .../source/signature.ts | 64 +++++++++++++++++++ packages/crypto-signature-ecdsa/tsconfig.json | 7 ++ pnpm-lock.yaml | 22 +++++++ 8 files changed, 231 insertions(+) create mode 100644 packages/crypto-signature-ecdsa/README.md create mode 100644 packages/crypto-signature-ecdsa/package.json create mode 100644 packages/crypto-signature-ecdsa/source/index.ts create mode 100644 packages/crypto-signature-ecdsa/source/signature.test.ts create mode 100644 packages/crypto-signature-ecdsa/source/signature.ts create mode 100644 packages/crypto-signature-ecdsa/tsconfig.json diff --git a/.github/workflows/unit.yml b/.github/workflows/unit.yml index 54a801dcd..613ad7609 100644 --- a/.github/workflows/unit.yml +++ b/.github/workflows/unit.yml @@ -94,6 +94,8 @@ jobs: run: cd packages/crypto-messages && pnpm run test - name: Test crypto-signature-bls12-381 run: cd packages/crypto-signature-bls12-381 && pnpm run test + - name: Test crypto-signature-ecdsa + run: cd packages/crypto-signature-ecdsa && pnpm run test - name: Test crypto-signature-schnorr run: cd packages/crypto-signature-schnorr && pnpm run test - name: Test crypto-transaction diff --git a/packages/crypto-signature-ecdsa/README.md b/packages/crypto-signature-ecdsa/README.md new file mode 100644 index 000000000..5e78cff8a --- /dev/null +++ b/packages/crypto-signature-ecdsa/README.md @@ -0,0 +1,19 @@ +# Mainsail - Crypto signature ECDSA + +![banner](https://raw.githubusercontent.com/ArkEcosystem/mainsail/main/banner.jpeg) + +## Documentation + +You can find installation instructions and detailed instructions on how to use this package at the [dedicated documentation site](https://ark.dev/docs/mainsail). + +## Security + +If you discover a security vulnerability within this package, please send an e-mail to [security@ark.io](mailto:security@ark.io). All security vulnerabilities will be promptly addressed. + +## Credits + +This project exists thanks to all the people who [contribute](https://github.com/ArkEcosystem/mainsail/graphs/contributors). + +## License + +[GPL-3.0-only](https://github.com/ArkEcosystem/mainsail/blob/main/LICENSE) © [ARK Ecosystem](https://ark.io) diff --git a/packages/crypto-signature-ecdsa/package.json b/packages/crypto-signature-ecdsa/package.json new file mode 100644 index 000000000..3c1fefffb --- /dev/null +++ b/packages/crypto-signature-ecdsa/package.json @@ -0,0 +1,37 @@ +{ + "name": "@mainsail/crypto-signature-ecdsa", + "version": "0.0.1-alpha.20", + "description": "Elliptic Curve Cryptography (ECDSA) signatures for the Mainsail blockchain", + "license": "GPL-3.0-only", + "contributors": [], + "type": "module", + "main": "distribution/index.js", + "types": "distribution/index.d.ts", + "files": [ + "distribution" + ], + "scripts": { + "build": "pnpm run clean && tsc", + "build:watch": "pnpm run clean && tsc -w", + "clean": "del distribution", + "release": "pnpm publish --access public", + "test": "pnpm run uvu source .test.ts", + "test:coverage": "c8 pnpm run test", + "test:coverage:html": "c8 -r html --all pnpm run test", + "test:file": "pnpm run uvu source", + "uvu": "tsx --tsconfig ../../tsconfig.test.json ./node_modules/uvu/bin.js" + }, + "dependencies": { + "@mainsail/container": "workspace:*", + "@mainsail/contracts": "workspace:*", + "@mainsail/kernel": "workspace:*", + "@mainsail/utils": "workspace:*", + "bcrypto": "5.5.2" + }, + "devDependencies": { + "uvu": "^0.5.6" + }, + "engines": { + "node": ">=20.x" + } +} diff --git a/packages/crypto-signature-ecdsa/source/index.ts b/packages/crypto-signature-ecdsa/source/index.ts new file mode 100644 index 000000000..589a48ff1 --- /dev/null +++ b/packages/crypto-signature-ecdsa/source/index.ts @@ -0,0 +1,30 @@ +import { Selectors } from "@mainsail/container"; +import { Identifiers } from "@mainsail/contracts"; +import { Providers } from "@mainsail/kernel"; +import { ByteBuffer } from "@mainsail/utils"; + +import { Signature } from "./signature.js"; + +export class ServiceProvider extends Providers.ServiceProvider { + public async register(): Promise { + this.app + .bind(Identifiers.Cryptography.Signature.Size) + .toFunction((buffer: ByteBuffer) => { + buffer.mark(); + buffer.skip(1); + + const lengthHex: string = buffer.readBytes(1).toString("hex"); + + buffer.reset(); + + return Number.parseInt(lengthHex, 16) + 2; + }) + .when(Selectors.anyAncestorOrTargetTaggedFirst("type", "wallet")); + + this.app + .bind(Identifiers.Cryptography.Signature.Instance) + .to(Signature) + .inSingletonScope() + .when(Selectors.anyAncestorOrTargetTaggedFirst("type", "wallet")); + } +} diff --git a/packages/crypto-signature-ecdsa/source/signature.test.ts b/packages/crypto-signature-ecdsa/source/signature.test.ts new file mode 100644 index 000000000..febe25ee0 --- /dev/null +++ b/packages/crypto-signature-ecdsa/source/signature.test.ts @@ -0,0 +1,50 @@ +import { describe } from "../../test-framework/source"; +import { Signature } from "./signature"; +import { secp256k1 } from "bcrypto"; + +describe("Signature", ({ assert, it }) => { + it("should sign and verify", async () => { + assert.true( + await new Signature().verify( + Buffer.from( + await new Signature().sign( + Buffer.from("64726e3da8", "hex"), + Buffer.from("814857ce48e291893feab95df02e1dbf7ad3994ba46f247f77e4eefd5d8734a2", "hex"), + ), + "hex", + ), + Buffer.from("64726e3da8", "hex"), + Buffer.from("03e84093c072af70004a38dd95e34def119d2348d5261228175d032e5f2070e19f", "hex"), + ), + ); + }); + + it("should sign recoverable and return r,s,v", async () => { + const privateKey = Buffer.from("814857ce48e291893feab95df02e1dbf7ad3994ba46f247f77e4eefd5d8734a2", "hex"); + const message = Buffer.from("64726e3da8", "hex"); + + const [signature, recoverId] = secp256k1.signRecoverable(message, privateKey); + + const r = signature.subarray(0, 32); + const s = signature.subarray(32, 64); + + const sig = { + r: r.toString("hex"), + s: s.toString("hex"), + v: recoverId + 27, + }; + + assert.equal(sig, { + r: "66f1c6d9fe13834f6e348aae40426060339ed8cba7d9b2f105c8220be095877c", + s: "1368fffd8294f1e22086703d33511fc8bb25231d6e9dc64d6449035003184bdd", + v: 28, + }); + + const signatureBuffer = Buffer.concat([Buffer.from(sig.r, "hex"), Buffer.from(sig.s, "hex")]); + const publicKey = secp256k1.recover(message, signatureBuffer, sig.v - 27, true); + assert.equal( + publicKey, + Buffer.from("03e84093c072af70004a38dd95e34def119d2348d5261228175d032e5f2070e19f", "hex"), + ); + }); +}); diff --git a/packages/crypto-signature-ecdsa/source/signature.ts b/packages/crypto-signature-ecdsa/source/signature.ts new file mode 100644 index 000000000..e0243dd41 --- /dev/null +++ b/packages/crypto-signature-ecdsa/source/signature.ts @@ -0,0 +1,64 @@ +import { inject, injectable } from "@mainsail/container"; +import { Contracts, Exceptions, Identifiers } from "@mainsail/contracts"; +import { ByteBuffer } from "@mainsail/utils"; +import { secp256k1 } from "bcrypto"; + +@injectable() +export class Signature implements Contracts.Crypto.Signature { + @inject(Identifiers.Cryptography.Signature.Size) + private readonly signatureSize!: Function; + + public async sign(message: Buffer, privateKey: Buffer): Promise { + return secp256k1.signatureExport(secp256k1.sign(message, privateKey)).toString("hex"); + } + + public async verify(signature: Buffer, message: Buffer, publicKey: Buffer): Promise { + const signatureRS = secp256k1.signatureImport(signature); + + if (!secp256k1.isLowS(signatureRS)) { + return false; + } + + // check that global signature length matches R and S length, see DER format : + //
+ const signatureLength = signature.readUInt8(1); + const rLength = signature.readUInt8(3); + const sLength = signature.readUInt8(4 + rLength + 1); + if ( + signature.length !== 4 + rLength + 2 + sLength || + signatureLength !== 2 + rLength + 2 + sLength || + signatureLength > 127 + ) { + return false; + } + + // check that first byte is positive, if it is then the whole R / S will be positive as required + const rFirstByte = signature.readInt8(4); + const sFirstByte = signature.readInt8(4 + rLength + 2); + if (rFirstByte < 0 || sFirstByte < 0 || rFirstByte > 127 || sFirstByte > 127) { + return false; + } + + // if first byte is zero it is to make R/S positive, so second byte should be negative + if ( + (rFirstByte === 0 && signature.readInt8(4 + 1) >= 0) || + (sFirstByte === 0 && signature.readInt8(4 + rLength + 2 + 1) >= 0) + ) { + return false; + } + + return secp256k1.verify(message, signatureRS, publicKey); + } + + public serialize(buffer: ByteBuffer, signature: string): void { + buffer.writeBytes(Buffer.from(signature, "hex")); + } + + public deserialize(buffer: ByteBuffer): Buffer { + return buffer.readBytes(this.signatureSize(buffer)); + } + + public async aggregate(signatures: Buffer[]): Promise { + throw new Exceptions.NotImplemented(this.constructor.name, "aggregate"); + } +} diff --git a/packages/crypto-signature-ecdsa/tsconfig.json b/packages/crypto-signature-ecdsa/tsconfig.json new file mode 100644 index 000000000..76e3e3970 --- /dev/null +++ b/packages/crypto-signature-ecdsa/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "distribution" + }, + "include": ["source/**/**.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d3bf807fa..3ee9eb30f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1542,6 +1542,28 @@ importers: specifier: ^0.5.6 version: 0.5.6 + packages/crypto-signature-ecdsa: + dependencies: + '@mainsail/container': + specifier: workspace:* + version: link:../container + '@mainsail/contracts': + specifier: workspace:* + version: link:../contracts + '@mainsail/kernel': + specifier: workspace:* + version: link:../kernel + '@mainsail/utils': + specifier: workspace:* + version: link:../utils + bcrypto: + specifier: 5.5.2 + version: 5.5.2 + devDependencies: + uvu: + specifier: ^0.5.6 + version: 0.5.6 + packages/crypto-signature-schnorr: dependencies: '@mainsail/container':