diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..51daa62 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +### 0.0.4 + +* fix ecdsa-p256 signature format to r || s +### 0.0.3 + +### Additions and Improvements + +* Add P256 support for key creation and non-deterministic signing with ECDSA and SHA256 + +### Bug Fixes diff --git a/package.json b/package.json index ddba0a1..c3f9949 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lacchain-key-manager", - "version": "0.0.2", + "version": "0.0.6", "description": "Rest api for lacchain key manager", "main": "dist/src/index.js", "types": "dist/src/index.d.ts", diff --git a/src/constants/errorMessages.ts b/src/constants/errorMessages.ts index 24680b9..9c800b1 100644 --- a/src/constants/errorMessages.ts +++ b/src/constants/errorMessages.ts @@ -21,7 +21,8 @@ export enum ErrorsMessages { USER_ALREADY_EXISTS = 'A user with this email is already registered', KEY_NOT_FOUND = 'Key not found', INVALID_ADDRESS = 'Invalid ripemd160 address', - INVALID_25519_TYPE = 'Invalid 25519 type' + INVALID_25519_TYPE = 'Invalid 25519 type', + INVALID_HEX_MESSAGE_ERROR = 'Expected a hex string' } export const Errors = { diff --git a/src/controllers/index.ts b/src/controllers/index.ts index 01c6ee9..265599f 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -1,9 +1,11 @@ import { DidJwtController } from './did.jwt.controller'; import { DidCommController } from './didcomm.controller'; import { Ed25519Controller } from './ed25519.controller'; -import { Secp256k1SignerController } from './secp256k1-signer.controller'; +import { P256Controller } from './p256.controller'; +import { Secp256k1SignerController } from './secp256k1.signer.controller'; import { Secp256k1Controller } from './secp256k1.controller'; import { X25519Controller } from './x25519.controller'; +import { P256SignerController } from './p256-signer.controller'; export const controllers = [ Secp256k1Controller, @@ -11,5 +13,7 @@ export const controllers = [ Secp256k1SignerController, DidJwtController, DidCommController, - Ed25519Controller + Ed25519Controller, + P256Controller, + P256SignerController ]; diff --git a/src/controllers/p256-signer.controller.ts b/src/controllers/p256-signer.controller.ts new file mode 100644 index 0000000..7ba0ce6 --- /dev/null +++ b/src/controllers/p256-signer.controller.ts @@ -0,0 +1,29 @@ +import { + JsonController, + Post, + BadRequestError, + Body +} from 'routing-controllers'; +import { Service } from 'typedi'; +import { ErrorsMessages } from '../constants/errorMessages'; +import { P256PlainMessageDTO } from '@dto/plainMessageDTO'; +import { P256SignerServiceDb } from '@services/p256.signer.service'; + +@JsonController('/p256/sign') +@Service() +export class P256SignerController { + constructor(private readonly p256SignerService: P256SignerServiceDb) {} + + @Post('/plain-message') + async signPlainMessage( + @Body({ validate: true }) message: P256PlainMessageDTO + ): Promise { + try { + return this.p256SignerService.signPlainMessage(message); + } catch (error: any) { + throw new BadRequestError( + error.detail ?? error.message ?? ErrorsMessages.INTERNAL_SERVER_ERROR + ); + } + } +} diff --git a/src/controllers/p256.controller.ts b/src/controllers/p256.controller.ts new file mode 100644 index 0000000..14d0323 --- /dev/null +++ b/src/controllers/p256.controller.ts @@ -0,0 +1,24 @@ +import { JsonController, Post, BadRequestError } from 'routing-controllers'; +import { Service } from 'typedi'; +import { ErrorsMessages } from '../constants/errorMessages'; +import { P256DbService } from '@services/p256Db.service'; + +@JsonController('/p256') +@Service() +export class P256Controller { + private readonly p256Service: P256DbService; + constructor() { + this.p256Service = new P256DbService(); + } + + @Post('/') + async create(): Promise { + try { + return this.p256Service.createKey(); + } catch (error: any) { + throw new BadRequestError( + error.detail ?? error.message ?? ErrorsMessages.INTERNAL_SERVER_ERROR + ); + } + } +} diff --git a/src/controllers/secp256k1-signer.controller.ts b/src/controllers/secp256k1.signer.controller.ts similarity index 89% rename from src/controllers/secp256k1-signer.controller.ts rename to src/controllers/secp256k1.signer.controller.ts index 7b3a8c3..8d90932 100644 --- a/src/controllers/secp256k1-signer.controller.ts +++ b/src/controllers/secp256k1.signer.controller.ts @@ -6,7 +6,8 @@ import { } from 'routing-controllers'; import { Service } from 'typedi'; import { ErrorsMessages } from '../constants/errorMessages'; -import { Secp256k1SignTransactionServiceDb } from '../services/signer.service'; +// eslint-disable-next-line max-len +import { Secp256k1SignTransactionServiceDb } from '../services/secp256k1.tx.signer.service'; // eslint-disable-next-line max-len import { Secp256k1SignLacchainTransactionServiceDb } from '../services/lacchain.signer.service'; import { EthereumTxDTO } from '../dto/signEthereumTxDTO'; @@ -57,7 +58,11 @@ export class Secp256k1SignerController { @Body({ validate: true }) message: Secp256k1PlainMessageDTO ): Promise { try { - return this.secp256k1SignerService.signPlainMessage(message); + return this.secp256k1SignerService.signPlainMessage({ + address: message.address, + keyId: message.keyId, + message: message.messageHash + }); } catch (error: any) { throw new BadRequestError( error.detail ?? error.message ?? ErrorsMessages.INTERNAL_SERVER_ERROR diff --git a/src/dto/plainMessageDTO.ts b/src/dto/plainMessageDTO.ts index 3c2aeb8..a5f99b5 100644 --- a/src/dto/plainMessageDTO.ts +++ b/src/dto/plainMessageDTO.ts @@ -1,6 +1,14 @@ import { IsOptional, IsString } from 'class-validator'; export class PlainMessageDTO { + @IsString() + message!: string; + @IsString() + @IsOptional() + keyId?: string; +} + +export class PlainMessageHashDTO { @IsString() messageHash!: string; @IsString() @@ -8,7 +16,26 @@ export class PlainMessageDTO { keyId?: string; } -export class Secp256k1PlainMessageDTO extends PlainMessageDTO { +export class Secp256k1PlainMessageDTO extends PlainMessageHashDTO { @IsString() address!: string; } + +export class P256PlainMessageDTO extends PlainMessageDTO { + @IsString() + compressedPublicKey!: string; + @IsOptional() + @IsString() + encoding!: + | 'base64' + | 'base64url' + | 'hex' + | 'binary' + | 'utf8' + | 'utf-8' + | 'utf16le' + | 'latin1' + | 'ascii' + | 'ucs2' + | 'ucs-2'; +} diff --git a/src/entities/ec.entity.ts b/src/entities/ec.entity.ts index 3dc00c8..9887faa 100644 --- a/src/entities/ec.entity.ts +++ b/src/entities/ec.entity.ts @@ -4,7 +4,8 @@ import { Base } from './base.entity'; export enum KeyType { SECP256k1 = 'SECP256k1', X25519 = 'X25519', - ED25519 = 'ED25519' + ED25519 = 'ED25519', + P256 = 'P256' } @Entity() export class EC extends Base { @@ -23,6 +24,12 @@ export class EC extends Base { @Column({ name: 'public_key', unique: true, nullable: true }) publicKey!: string; + @Column({ name: 'x', unique: true, nullable: true }) + x!: string; + + @Column({ name: 'y', unique: true, nullable: true }) + y!: string; + @Column({ name: 'key_type', type: 'enum', diff --git a/src/index.ts b/src/index.ts index 72ef147..fdc5967 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,10 @@ export { Secp256k1DbService } from './services/secp256k1Db.service'; export { Generic25519DbService } from './services/generic25519Db.service'; +export { P256DbService } from './services/p256Db.service'; export { ECService } from './services/interfaces/ec'; export { EC } from './entities/ec.entity'; -export { Secp256k1SignTransactionServiceDb } from './services/signer.service'; +// eslint-disable-next-line max-len +export { Secp256k1SignTransactionServiceDb } from './services/secp256k1.tx.signer.service'; // eslint-disable-next-line max-len export { Secp256k1SignLacchainTransactionService } from './services/interfaces/secp256k1.lacchain.signer'; // eslint-disable-next-line max-len @@ -17,7 +19,9 @@ export { IDidJwt } from './interfaces/did-jwt/did.jwt.interface'; export { IDidCommService } from './services/interfaces/didcomm.service'; export { DidCommDbService } from './services/didcomm/didcomm.db.service'; export { IDidCommToEncryptData } from './interfaces/didcomm/didcomm.interface'; + // eslint-disable-next-line max-len export { Secp256k1GenericSignerService } from './services/interfaces/secp256k1.generic.signer'; // eslint-disable-next-line max-len -export { Secp256k1GenericSignerServiceDb } from './services/lacchain.generic.signer.service'; +export { Secp256k1GenericSignerServiceDb } from './services/secp256k1.signer.service'; +export { P256SignerServiceDb } from './services/p256.signer.service'; diff --git a/src/interfaces/signer/signer.interface.ts b/src/interfaces/signer/signer.interface.ts index 985e8c7..e2362cb 100644 --- a/src/interfaces/signer/signer.interface.ts +++ b/src/interfaces/signer/signer.interface.ts @@ -15,7 +15,7 @@ export interface ISignedTransaction { } export interface IPlainMessage { - messageHash: string; + message: string; keyId?: string; } @@ -23,6 +23,22 @@ export interface ISignPlainMessageByAddress extends IPlainMessage { address: string; } -export interface ISecp256k1SignatureMessageResponse { +export interface ISignPlainMessageByCompressedPublicKey extends IPlainMessage { + compressedPublicKey: string; + encoding?: + | 'base64' + | 'base64url' + | 'hex' + | 'binary' + | 'utf8' + | 'utf-8' + | 'utf16le' + | 'latin1' + | 'ascii' + | 'ucs2' + | 'ucs-2'; +} + +export interface IECDSASignatureMessageResponse { signature: string; } diff --git a/src/services/interfaces/ec.ts b/src/services/interfaces/ec.ts index a97749f..f1c7681 100644 --- a/src/services/interfaces/ec.ts +++ b/src/services/interfaces/ec.ts @@ -4,10 +4,12 @@ export interface IECFullKey { key: string; type: string; publicKey: string; + x?: string; + y?: string; } export type key = { keyId: string; - address: string; + address?: string; publicKey: string; type: string; }; diff --git a/src/services/interfaces/ecdsa.signer.ts b/src/services/interfaces/ecdsa.signer.ts new file mode 100644 index 0000000..8040748 --- /dev/null +++ b/src/services/interfaces/ecdsa.signer.ts @@ -0,0 +1,14 @@ +import { + IECDSASignatureMessageResponse, + ISignPlainMessageByCompressedPublicKey +} from 'src/interfaces/signer/signer.interface'; + +export interface ECDSASignerService { + /** + * + * @param message - The 'hashed' message to be signed - MUST start with '0x' + */ + signPlainMessage( + message: ISignPlainMessageByCompressedPublicKey + ): Promise; +} diff --git a/src/services/interfaces/secp256k1.generic.signer.ts b/src/services/interfaces/secp256k1.generic.signer.ts index 40ff072..753f3db 100644 --- a/src/services/interfaces/secp256k1.generic.signer.ts +++ b/src/services/interfaces/secp256k1.generic.signer.ts @@ -1,14 +1,15 @@ import { ISignPlainMessageByAddress, - ISecp256k1SignatureMessageResponse + IECDSASignatureMessageResponse } from 'src/interfaces/signer/signer.interface'; export interface Secp256k1GenericSignerService { /** * - * @param message - The 'hashed' message to be signed - MUST start with '0x' + * @param {ISignPlainMessageByAddress} message - + * The 'hashed' message to be signed - MUST start with '0x' */ signPlainMessage( message: ISignPlainMessageByAddress - ): Promise; + ): Promise; } diff --git a/src/services/p256.signer.service.ts b/src/services/p256.signer.service.ts new file mode 100644 index 0000000..fa0e413 --- /dev/null +++ b/src/services/p256.signer.service.ts @@ -0,0 +1,79 @@ +import { log4TSProvider } from '../config'; +import { Service } from 'typedi'; +import { + IECDSASignatureMessageResponse, + ISignPlainMessageByCompressedPublicKey +} from 'src/interfaces/signer/signer.interface'; +import { ECDSASignerService } from './interfaces/ecdsa.signer'; +import { P256DbService } from './p256Db.service'; +import { createSign, KeyObject, webcrypto } from 'node:crypto'; +import { BadRequestError, InternalServerError } from 'routing-controllers'; +import { ErrorsMessages } from '../constants/errorMessages'; +import { isHexString } from 'ethers'; + +@Service() +// eslint-disable-next-line max-len +export class P256SignerServiceDb implements ECDSASignerService { + protected readonly p256DbService = new P256DbService(); + log = log4TSProvider.getLogger('p256-plain-message-signer'); + /** + * @reference https://nodejs.org/docs/latest-v16.x/api/crypto.html#class-sign ... + * If encoding is not provided in {@link ISignPlainMessageByCompressedPublicKey} + * request, and the data is a string, an encoding of 'utf8' + * is enforced. If data is a Buffer, TypedArray, orDataView, + * then inputEncoding is ignored. + * @param {ISignPlainMessageByCompressedPublicKey} request - message request to sign. + * @return {Promise} + */ + async signPlainMessage( + request: ISignPlainMessageByCompressedPublicKey + ): Promise { + const publicKey = request.compressedPublicKey; + const foundKey = await this.p256DbService.getKeyByCompressedPublicKey( + publicKey + ); + + if (!(foundKey.x && foundKey.y)) { + throw new InternalServerError(ErrorsMessages.INTERNAL_SERVER_ERROR); + } + + const jwk = { + crv: 'P-256', + kty: 'EC', + x: Buffer.from(foundKey.x.replace('0x', ''), 'hex').toString('base64url'), + y: Buffer.from(foundKey.y.replace('0x', ''), 'hex').toString('base64url'), + d: Buffer.from(foundKey.key.replace('0x', ''), 'hex').toString( + 'base64url' + ) + }; + + const importedKey = await webcrypto.subtle.importKey( + 'jwk', + jwk, + { name: 'ECDSA', namedCurve: 'P-256' }, + true, + ['sign'] + ); + + let message = request.message; + const sign = createSign('SHA256'); + if (request.encoding && request.encoding === 'hex') { + message = message.replace('0x', ''); + if (!isHexString(message.startsWith('0x') ? message : '0x' + message)) { + throw new BadRequestError(ErrorsMessages.INVALID_HEX_MESSAGE_ERROR); + } + } + sign.update(message, request.encoding ? request.encoding : 'utf8'); + sign.end(); + const sig = + '0x' + + sign.sign( + { key: KeyObject.from(importedKey), dsaEncoding: 'ieee-p1363' }, + 'hex' + ); + const signature = { + signature: sig + }; + return signature; + } +} diff --git a/src/services/p256Db.service.ts b/src/services/p256Db.service.ts new file mode 100644 index 0000000..0184b82 --- /dev/null +++ b/src/services/p256Db.service.ts @@ -0,0 +1,88 @@ +import { Service } from 'typedi'; +import { getRepository } from 'typeorm'; +import { EC, KeyType } from '../entities/ec.entity'; +import { EntityMapper } from '@clients/mapper/entityMapper.service'; +import { ECService, IECFullKey, key } from './interfaces/ec'; +import { log4TSProvider } from '../config'; +import { ErrorsMessages } from '../constants/errorMessages'; +import { BadRequestError } from 'routing-controllers'; +import crypto from 'crypto'; + +@Service() +export class P256DbService implements ECService { + private readonly p256Repository = getRepository(EC); + log = log4TSProvider.getLogger('P256DbService'); + + show(id: string) { + return this.p256Repository.findOne(id); + } + + /** + * Creates a P256 key returning the keyId for which the P256 key can be queried + * in future calls, as well as the uncompressed public key + * @return {Promise} + */ + async createKey(): Promise { + const p256 = EntityMapper.mapTo(EC, {}); + const p256Key = crypto.createECDH('prime256v1'); + p256Key.generateKeys(); + const pubKey = p256Key.getPublicKey().toString('hex'); + p256.x = Buffer.from(p256Key.getPublicKey()) + .toString('hex') + .substring(2, 66); + p256.y = Buffer.from(p256Key.getPublicKey()).toString('hex').substring(66); + p256.key = '0x' + p256Key.getPrivateKey().toString('hex'); + p256.keyType = KeyType.P256; + await this.p256Repository.insert(p256); + return { + keyId: p256.keyId, + publicKey: pubKey, + type: p256.keyType + }; + } + + deleteKey(id: string) { + return this.p256Repository.delete(id); + } + async getKeyByPublicKey(publicKey: string): Promise { + const r = await this.p256Repository.findOne(undefined, { + where: { + publicKey + } + }); + if (!r) { + const message = ErrorsMessages.KEY_NOT_FOUND; + this.log.info(message); + throw new BadRequestError(message); + } + return { + key: r.key, + keyId: r.keyId, + address: r.address, + type: r.keyType, + publicKey: r.publicKey + }; + } + + async getKeyByCompressedPublicKey(publicKey: string): Promise { + const r = await this.p256Repository.findOne(undefined, { + where: { + x: publicKey.replace('0x02', '') + } + }); + if (!r) { + const message = ErrorsMessages.KEY_NOT_FOUND; + this.log.info(message); + throw new BadRequestError(message); + } + return { + key: r.key, + keyId: r.keyId, + address: r.address, + type: r.keyType, + publicKey: r.publicKey, + x: r.x, + y: r.y + }; + } +} diff --git a/src/services/lacchain.generic.signer.service.ts b/src/services/secp256k1.signer.service.ts similarity index 88% rename from src/services/lacchain.generic.signer.service.ts rename to src/services/secp256k1.signer.service.ts index 19e8b92..8821e9f 100644 --- a/src/services/lacchain.generic.signer.service.ts +++ b/src/services/secp256k1.signer.service.ts @@ -4,7 +4,7 @@ import { log4TSProvider } from '../config'; import { BadRequestError } from 'routing-controllers'; import { Service } from 'typedi'; import { - ISecp256k1SignatureMessageResponse, + IECDSASignatureMessageResponse, ISignPlainMessageByAddress } from 'src/interfaces/signer/signer.interface'; import { SigningKey, isAddress } from 'ethers'; @@ -18,11 +18,11 @@ export class Secp256k1GenericSignerServiceDb implements Secp256k1GenericSignerSe log = log4TSProvider.getLogger('secp256k1-plain-message-signer'); /** * @param {string} digest - message digest (32 bytes) to sign. - * @return {Promise} + * @return {Promise} */ async signPlainMessage( digest: ISignPlainMessageByAddress - ): Promise { + ): Promise { const signerAddress = digest.address; if (!signerAddress || !isAddress(signerAddress)) { const message = ErrorsMessages.MISSING_PARAMS; @@ -32,7 +32,7 @@ export class Secp256k1GenericSignerServiceDb implements Secp256k1GenericSignerSe const privateKey = await this.secp256k1DbService.getKeyByAddress( signerAddress ); - const messageHash = digest.messageHash; + const messageHash = digest.message; const messageHashBytes = arrayify(messageHash); const signingKeyInstance = new SigningKey(privateKey.key); const s = signingKeyInstance.sign(messageHashBytes).serialized; diff --git a/src/services/signer.service.ts b/src/services/secp256k1.tx.signer.service.ts similarity index 88% rename from src/services/signer.service.ts rename to src/services/secp256k1.tx.signer.service.ts index 0ab6a55..141d809 100644 --- a/src/services/signer.service.ts +++ b/src/services/secp256k1.tx.signer.service.ts @@ -8,10 +8,11 @@ import { ISignedTransaction } from 'src/interfaces/signer/signer.interface'; import { Secp256k1SignTransactionService } from './interfaces/secp256k1.signer'; -import { Secp256k1GenericSignerServiceDb } from './lacchain.generic.signer.service'; +import { Secp256k1GenericSignerServiceDb } from './secp256k1.signer.service'; @Service() -export class Secp256k1SignTransactionServiceDb extends Secp256k1GenericSignerServiceDb +export class Secp256k1SignTransactionServiceDb + extends Secp256k1GenericSignerServiceDb implements Secp256k1SignTransactionService { log = log4TSProvider.getLogger('ethereum-signer'); async signEthereumBasedTransaction(