Skip to content

Commit

Permalink
Merge pull request #35 from lacchain/p256-support
Browse files Browse the repository at this point in the history
P256 support
  • Loading branch information
eum602 authored Oct 10, 2023
2 parents 6a64986 + 615e1e5 commit 02db45e
Show file tree
Hide file tree
Showing 18 changed files with 336 additions and 22 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
3 changes: 2 additions & 1 deletion src/constants/errorMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
8 changes: 6 additions & 2 deletions src/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
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,
X25519Controller,
Secp256k1SignerController,
DidJwtController,
DidCommController,
Ed25519Controller
Ed25519Controller,
P256Controller,
P256SignerController
];
29 changes: 29 additions & 0 deletions src/controllers/p256-signer.controller.ts
Original file line number Diff line number Diff line change
@@ -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<any> {
try {
return this.p256SignerService.signPlainMessage(message);
} catch (error: any) {
throw new BadRequestError(
error.detail ?? error.message ?? ErrorsMessages.INTERNAL_SERVER_ERROR
);
}
}
}
24 changes: 24 additions & 0 deletions src/controllers/p256.controller.ts
Original file line number Diff line number Diff line change
@@ -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<any> {
try {
return this.p256Service.createKey();
} catch (error: any) {
throw new BadRequestError(
error.detail ?? error.message ?? ErrorsMessages.INTERNAL_SERVER_ERROR
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -57,7 +58,11 @@ export class Secp256k1SignerController {
@Body({ validate: true }) message: Secp256k1PlainMessageDTO
): Promise<any> {
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
Expand Down
29 changes: 28 additions & 1 deletion src/dto/plainMessageDTO.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,41 @@
import { IsOptional, IsString } from 'class-validator';

export class PlainMessageDTO {
@IsString()
message!: string;
@IsString()
@IsOptional()
keyId?: string;
}

export class PlainMessageHashDTO {
@IsString()
messageHash!: string;
@IsString()
@IsOptional()
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';
}
9 changes: 8 additions & 1 deletion src/entities/ec.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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',
Expand Down
8 changes: 6 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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';
20 changes: 18 additions & 2 deletions src/interfaces/signer/signer.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,30 @@ export interface ISignedTransaction {
}

export interface IPlainMessage {
messageHash: string;
message: string;
keyId?: string;
}

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;
}
4 changes: 3 additions & 1 deletion src/services/interfaces/ec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand Down
14 changes: 14 additions & 0 deletions src/services/interfaces/ecdsa.signer.ts
Original file line number Diff line number Diff line change
@@ -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<IECDSASignatureMessageResponse>;
}
7 changes: 4 additions & 3 deletions src/services/interfaces/secp256k1.generic.signer.ts
Original file line number Diff line number Diff line change
@@ -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<ISecp256k1SignatureMessageResponse>;
): Promise<IECDSASignatureMessageResponse>;
}
79 changes: 79 additions & 0 deletions src/services/p256.signer.service.ts
Original file line number Diff line number Diff line change
@@ -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<IECDSASignatureMessageResponse>}
*/
async signPlainMessage(
request: ISignPlainMessageByCompressedPublicKey
): Promise<IECDSASignatureMessageResponse> {
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;
}
}
Loading

0 comments on commit 02db45e

Please sign in to comment.