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

Use @noble/curves for Node ECDSA signing #694

Merged
merged 1 commit into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 1 addition & 4 deletions node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@
"dependencies": {
"@grpc/grpc-js": "^1.10.0",
"@hyperledger/fabric-protos": "^0.3.0",
"asn1.js": "^5.4.0",
"bn.js": "^5.2.0",
"elliptic": "^6.5.0",
"@noble/curves": "^1.4.0",
"google-protobuf": "^3.21.0"
},
"optionalDependencies": {
Expand All @@ -46,7 +44,6 @@
"devDependencies": {
"@cyclonedx/cyclonedx-npm": "^1.16.1",
"@tsconfig/node18": "^18.2.2",
"@types/elliptic": "^6.4.18",
"@types/google-protobuf": "^3.15.12",
"@types/jest": "^29.5.12",
"@types/node": "^18.19.22",
Expand Down
4 changes: 2 additions & 2 deletions node/src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ The following complete example shows how to connect to a Fabric network, submit
import * as grpc from '@grpc/grpc-js';
import * as crypto from 'node:crypto';
import { connect, Identity, signers } from '@hyperledger/fabric-gateway';
import { promises as fs } from 'fs';
import { TextDecoder } from 'util';
import { promises as fs } from 'node:fs';
import { TextDecoder } from 'node:util';

const utf8Decoder = new TextDecoder();

Expand Down
2 changes: 1 addition & 1 deletion node/src/blockevents.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ describe('Block Events', () => {
details: 'DETAILS',
metadata: new Metadata(),
});
const tlsClientCertificateHash = Uint8Array.from(Buffer.from('TLS_CLIENT_CERTIFICATE_HASH'));
const tlsClientCertificateHash = new Uint8Array(Buffer.from('TLS_CLIENT_CERTIFICATE_HASH'));

let defaultOptions: () => CallOptions;
let client: MockGatewayGrpcClient;
Expand Down
4 changes: 2 additions & 2 deletions node/src/checkpointers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { promises as fs } from 'fs';
import * as path from 'path';
import { promises as fs } from 'node:fs';
import * as path from 'node:path';
import { ChaincodeEvent } from './chaincodeevent';
import { Checkpointer } from './checkpointer';
import * as checkpointers from './checkpointers';
Expand Down
2 changes: 1 addition & 1 deletion node/src/filecheckpointer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import fs from 'fs';
import fs from 'node:fs';
import { ChaincodeEvent } from './chaincodeevent';
import { Checkpointer } from './checkpointer';

Expand Down
2 changes: 1 addition & 1 deletion node/src/gateway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('Gateway', () => {
const result = gateway.getIdentity();

expect(result.mspId).toEqual(identity.mspId);
expect(Uint8Array.from(result.credentials)).toEqual(Uint8Array.from(identity.credentials));
expect(new Uint8Array(result.credentials)).toEqual(new Uint8Array(identity.credentials));
});
});

Expand Down
2 changes: 1 addition & 1 deletion node/src/hash/hashes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { createHash } from 'crypto';
import { createHash } from 'node:crypto';
import { Hash } from './hash';

/**
Expand Down
19 changes: 0 additions & 19 deletions node/src/identity/asn1.ts

This file was deleted.

52 changes: 10 additions & 42 deletions node/src/identity/ecdsa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
* SPDX-License-Identifier: Apache-2.0
*/

import BN from 'bn.js';
import { KeyObject } from 'crypto';
import { ec as EC } from 'elliptic';
import { ecSignatureAsDER } from './asn1';
import { CurveFn } from '@noble/curves/abstract/weierstrass';
import { p256 } from '@noble/curves/p256';
import { p384 } from '@noble/curves/p384';
import { KeyObject } from 'node:crypto';
import { Signer } from './signer';

const namedCurves: Record<string, EC> = {
'P-256': new EC('p256'),
'P-384': new EC('p384'),
const namedCurves: Record<string, CurveFn> = {
'P-256': p256,
'P-384': p384,
};

export function newECPrivateKeySigner(key: KeyObject): Signer {
Expand All @@ -28,48 +28,16 @@ export function newECPrivateKeySigner(key: KeyObject): Signer {
const privateKey = Buffer.from(d, 'base64url');

return (digest) => {
const signature = curve.sign(digest, privateKey, { canonical: true });
const signatureBytes = new Uint8Array(signature.toDER());
return Promise.resolve(signatureBytes);
const signature = curve.sign(digest, privateKey, { lowS: true });
return Promise.resolve(signature.toDERRawBytes());
};
}

function getCurve(name: string): EC {
function getCurve(name: string): CurveFn {
const curve = namedCurves[name];
if (!curve) {
throw new Error(`Unsupported curve: ${name}`);
}

return curve;
}

export class ECSignature {
readonly #curve: EC;
readonly #r: BN;
#s: BN;

constructor(curveName: string, compactSignature: Uint8Array) {
this.#curve = getCurve(curveName);

const sIndex = compactSignature.length / 2;
const r = compactSignature.slice(0, sIndex);
const s = compactSignature.slice(sIndex);
this.#r = new BN(r);
this.#s = new BN(s);
}

normalise(): this {
const n = this.#curve.n!;
const halfOrder = n.divn(2);

if (this.#s.gt(halfOrder)) {
this.#s = n.sub(this.#s);
}

return this;
}

toDER(): Uint8Array {
return ecSignatureAsDER(this.#r, this.#s);
}
}
30 changes: 18 additions & 12 deletions node/src/identity/hsmsigner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { p256 } from '@noble/curves/p256';
import { createHash } from 'node:crypto';
import { Mechanism, Pkcs11Error, SessionInfo, SlotInfo, Template, TokenInfo } from 'pkcs11js';
import { HSMSignerOptions } from './hsmsigner';
import { newHSMSignerFactory } from './signers';
Expand Down Expand Up @@ -188,12 +190,11 @@ describe('When using an HSM Signer', () => {
const mockSession = Buffer.from('mockSession');
const mockPrivateKeyHandle = Buffer.from('someobject');

const HSMSignature =
'a5f6e5dd8c46ee4094ebb908b719572022f64ed4bbc21f1f5aa4e49163f4f56c4c6ca8b0393836c79045b1be2f25b1cd2b2b253a213fc9248b7e18574c4170b4';
const DERSignature =
'3045022100a5f6e5dd8c46ee4094ebb908b719572022f64ed4bbc21f1f5aa4e49163f4f56c02204c6ca8b0393836c79045b1be2f25b1cd2b2b253a213fc9248b7e18574c4170b4';
const hsmSignerFactory = newHSMSignerFactory('somelibrary');

const privateKey = p256.utils.randomPrivateKey();
const publicKey = p256.getPublicKey(privateKey);

beforeEach(() => {
resetPkcs11Stub();
pkcs11Stub.C_GetTokenInfo = mockTokenInfo;
Expand All @@ -206,6 +207,13 @@ describe('When using an HSM Signer', () => {
pkcs11Stub.C_FindObjects = jest.fn(() => {
return [mockPrivateKeyHandle];
});
pkcs11Stub.C_SignInit = jest.fn();
pkcs11Stub.C_Sign = jest.fn((session, digest, buffer) => {
const signature = p256.sign(digest, privateKey).toCompactRawBytes();
signature.forEach((b, i) => buffer.writeUInt8(b, i));
// Return buffer of exactly signature length regardless of supplied buffer size
return buffer.subarray(0, signature.length);
});
});

it('throws if label, pin or identifier are blank or not provided', () => {
Expand Down Expand Up @@ -354,16 +362,14 @@ describe('When using an HSM Signer', () => {
});

it('signs using the HSM', async () => {
pkcs11Stub.C_SignInit = jest.fn();
pkcs11Stub.C_Sign = jest.fn(() => {
return Buffer.from(HSMSignature, 'hex');
});

const digest = Buffer.from('some digest');
const message = Buffer.from('A quick brown fox jumps over the lazy dog');
const digest = createHash('sha256').update(message).digest();

const { signer } = hsmSignerFactory.newSigner(hsmOptions);
const signed = await signer(digest);
expect(signed).toEqual(new Uint8Array(Buffer.from(DERSignature, 'hex')));
const signature = await signer(digest);

const valid = p256.verify(signature, digest, publicKey);
expect(valid).toBe(true);

expect(pkcs11Stub.C_SignInit).toHaveBeenCalledWith(mockSession, { mechanism: CKM_ECDSA }, mockPrivateKeyHandle);
expect(pkcs11Stub.C_Sign).toHaveBeenCalledWith(mockSession, digest, expect.anything());
Expand Down
12 changes: 9 additions & 3 deletions node/src/identity/hsmsigner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { p256 } from '@noble/curves/p256';
import * as pkcs11js from 'pkcs11js';
import { ECSignature } from './ecdsa';
import { Signer } from './signer';

export interface HSMSignerOptions {
Expand Down Expand Up @@ -96,8 +96,14 @@ export class HSMSignerFactoryImpl implements HSMSignerFactory {
return {
signer: (digest) => {
pkcs11.C_SignInit(session, { mechanism: pkcs11js.CKM_ECDSA }, privateKeyHandle);
const compactSignature = pkcs11.C_Sign(session, Buffer.from(digest), Buffer.alloc(256));
const signature = new ECSignature('P-256', compactSignature).normalise().toDER();
const compactSignature = pkcs11.C_Sign(
session,
Buffer.from(digest),
// EC signatures have length of 2n according to the PKCS11 spec:
// https://docs.oasis-open.org/pkcs11/pkcs11-spec/v3.1/pkcs11-spec-v3.1.html
Buffer.alloc(p256.CURVE.nByteLength * 2),
);
const signature = p256.Signature.fromCompact(compactSignature).normalizeS().toDERRawBytes();
return Promise.resolve(signature);
},
close: () => pkcs11.C_CloseSession(session),
Expand Down
2 changes: 1 addition & 1 deletion node/src/identity/signers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { createHash, generateKeyPairSync, verify } from 'crypto';
import { createHash, generateKeyPairSync, verify } from 'node:crypto';
import { newPrivateKeySigner } from './signers';

describe('signers', () => {
Expand Down
18 changes: 9 additions & 9 deletions node/src/signingidentity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ describe('SigningIdentity', () => {
beforeEach(() => {
identity = {
mspId: 'MSP_ID',
credentials: Uint8Array.from(Buffer.from('CREDENTIALS')),
credentials: new Uint8Array(Buffer.from('CREDENTIALS')),
};
});

describe('identity', () => {
it('changes to returned identity do not modify signing identity', () => {
const expectedMspId = identity.mspId;
const expectedCredentials = Uint8Array.from(identity.credentials); // Copy
const expectedCredentials = new Uint8Array(identity.credentials); // Copy
const signingIdentity = new SigningIdentity({ identity });

const output = signingIdentity.getIdentity();
Expand All @@ -31,21 +31,21 @@ describe('SigningIdentity', () => {

const actual = signingIdentity.getIdentity();
expect(actual.mspId).toBe(expectedMspId);
const actualCredentials = Uint8Array.from(actual.credentials); // Ensure it's really a Uint8Array
const actualCredentials = new Uint8Array(actual.credentials); // Ensure it's really a Uint8Array
expect(actualCredentials).toEqual(expectedCredentials);
});

it('changes to supplied identity do not modify signing identity', () => {
const expectedMspId = identity.mspId;
const expectedCredentials = Uint8Array.from(identity.credentials); // Copy
const expectedCredentials = new Uint8Array(identity.credentials); // Copy

const signingIdentity = new SigningIdentity({ identity });
identity.mspId = 'wrong';
identity.credentials.fill(0);

const actual = signingIdentity.getIdentity();
expect(actual.mspId).toBe(expectedMspId);
const actualCredentials = Uint8Array.from(actual.credentials); // Ensure it's really a Uint8Array
const actualCredentials = new Uint8Array(actual.credentials); // Ensure it's really a Uint8Array
expect(actualCredentials).toEqual(expectedCredentials);
});
});
Expand All @@ -58,18 +58,18 @@ describe('SigningIdentity', () => {

const actual = msp.SerializedIdentity.deserializeBinary(creator);
expect(actual.getMspid()).toBe(identity.mspId);
const credentials = Uint8Array.from(actual.getIdBytes_asU8()); // Ensure it's really a Uint8Array
const credentials = new Uint8Array(actual.getIdBytes_asU8()); // Ensure it's really a Uint8Array
expect(credentials).toEqual(identity.credentials);
});

it('changes to returned creator do not modify signing identity', () => {
const signingIdentity = new SigningIdentity({ identity });
const expected = Uint8Array.from(signingIdentity.getCreator()); // Ensure it's really a Uint8Array
const expected = new Uint8Array(signingIdentity.getCreator()); // Ensure it's really a Uint8Array

const creator = signingIdentity.getCreator();
creator.fill(0);

const actual = Uint8Array.from(signingIdentity.getCreator()); // Ensure it's really a Uint8Array
const actual = new Uint8Array(signingIdentity.getCreator()); // Ensure it's really a Uint8Array
expect(actual).toEqual(expected);
});
});
Expand All @@ -83,7 +83,7 @@ describe('SigningIdentity', () => {
});

it('uses supplied signer', async () => {
const expected = Uint8Array.from(Buffer.from('SIGNATURE'));
const expected = new Uint8Array(Buffer.from('SIGNATURE'));
const signer: Signer = async () => Promise.resolve(expected);
const digest = Buffer.from('DIGEST');
const signingIdentity = new SigningIdentity({ identity, signer });
Expand Down
6 changes: 3 additions & 3 deletions node/src/signingidentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export class SigningIdentity {
constructor(options: Readonly<SigningIdentityOptions>) {
this.#identity = {
mspId: options.identity.mspId,
credentials: Uint8Array.from(options.identity.credentials),
credentials: new Uint8Array(options.identity.credentials),
};

const serializedIdentity = new msp.SerializedIdentity();
Expand All @@ -43,12 +43,12 @@ export class SigningIdentity {
getIdentity(): Identity {
return {
mspId: this.#identity.mspId,
credentials: Uint8Array.from(this.#identity.credentials),
credentials: new Uint8Array(this.#identity.credentials),
};
}

getCreator(): Uint8Array {
return Uint8Array.from(this.#creator);
return new Uint8Array(this.#creator);
}

hash(message: Uint8Array): Uint8Array {
Expand Down
6 changes: 3 additions & 3 deletions node/src/testutils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

import * as grpc from '@grpc/grpc-js';
import { common, gateway, peer } from '@hyperledger/fabric-protos';
import fs from 'fs';
import os from 'os';
import path from 'path';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import {
CloseableAsyncIterable,
DuplexStreamResponse,
Expand Down
2 changes: 1 addition & 1 deletion node/src/transactioncontext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { common } from '@hyperledger/fabric-protos';
import { randomBytes } from 'crypto';
import { randomBytes } from 'node:crypto';
import { sha256 } from './hash/hashes';
import { SigningIdentity } from './signingidentity';

Expand Down
2 changes: 1 addition & 1 deletion node/src/transactionparser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
*/

import { common, peer } from '@hyperledger/fabric-protos';
import { inspect } from 'util';
import { inspect } from 'node:util';
import { assertDefined } from './gateway';

export function parseTransactionEnvelope(envelope: common.Envelope): {
Expand Down
Loading