diff --git a/src/did/controllers/did.controller.ts b/src/did/controllers/did.controller.ts index 01778391..a433cff3 100644 --- a/src/did/controllers/did.controller.ts +++ b/src/did/controllers/did.controller.ts @@ -50,6 +50,8 @@ import { RegisterDidDto } from '../dto/register-did.dto'; import { IKeyType } from 'hs-ssi-sdk'; import { AtLeastOneParamPipe } from 'src/utils/Pipes/atleastOneParam.pipe'; import { AddVMResponse, AddVerificationMethodDto } from '../dto/addVm.dto'; +import { SignDidDto, SignedDidDocument } from '../dto/sign-did.dto'; +import { VerifyDidDocResponseDto, VerifyDidDto } from '../dto/verify-did.dto'; @UseFilters(AllExceptionsFilter) @ApiTags('Did') @Controller('did') @@ -192,7 +194,7 @@ export class DidController { } @Post('/addVerificationMethod') - @ApiCreatedResponse({ + @ApiOkResponse({ description: 'Added vm to Did Document', type: AddVMResponse, }) @@ -217,12 +219,73 @@ export class DidController { addVerficationMethod( @Headers('Authorization') authorization: string, @Body() addVm: AddVerificationMethodDto, - @Req() req: any, ) { Logger.log('addVerificationMethod() method: starts', 'DidController'); return this.didService.addVerificationMethod(addVm); } - @ApiCreatedResponse({ + + @Post('/sign') + @ApiOkResponse({ + description: 'DidDocument is signed successfully', + type: SignedDidDocument, + }) + @ApiBadRequestResponse({ + status: 400, + description: 'Error occured at the time of signing did', + type: DidError, + }) + @ApiHeader({ + name: 'Authorization', + description: 'Bearer ', + required: false, + }) + @ApiHeader({ + name: 'Origin', + description: 'Origin as you set in application cors', + required: false, + }) + @UsePipes(ValidationPipe) + @UsePipes(new AtLeastOneParamPipe(['did', 'didDocument'])) + SignDidDocument( + @Headers('Authorization') authorization: string, + @Req() req: any, + @Body() signDidDocDto: SignDidDto, + ) { + Logger.log('SignDidDocument() method: starts', 'DidController'); + return this.didService.SignDidDocument(signDidDocDto, req.user); + } + @Post('/verify') + @ApiOkResponse({ + description: 'DidDocument is verified successfully', + type: VerifyDidDocResponseDto, + }) + @ApiBadRequestResponse({ + status: 400, + description: 'Error occured at the time of verifing did', + type: DidError, + }) + @ApiHeader({ + name: 'Authorization', + description: 'Bearer ', + required: false, + }) + @ApiHeader({ + name: 'Origin', + description: 'Origin as you set in application cors', + required: false, + }) + @UsePipes(ValidationPipe) + @UsePipes(new AtLeastOneParamPipe(['did', 'didDocument'])) + VerifyDidDocument( + @Headers('Authorization') authorization: string, + @Req() req: any, + @Body() verifyDidDto: VerifyDidDto, + ) { + Logger.log('VerifyDidDocument() method: starts', 'DidController'); + return this.didService.VerifyDidDocument(verifyDidDto, req.user); + } + + @ApiOkResponse({ description: 'DID Registred', type: RegisterDidResponse, }) diff --git a/src/did/dto/sign-did.dto.ts b/src/did/dto/sign-did.dto.ts new file mode 100644 index 00000000..083fea9c --- /dev/null +++ b/src/did/dto/sign-did.dto.ts @@ -0,0 +1,157 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { DidDoc } from './update-did.dto'; +import { + IsEnum, + IsNotEmptyObject, + IsOptional, + IsString, + Matches, + ValidateIf, + ValidateNested, +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { ValidateVerificationMethodId } from 'src/utils/customDecorator/vmId.decorator'; +import { IsDid } from 'src/utils/customDecorator/did.decorator'; +import { SupportedPurpose } from 'hs-ssi-sdk'; + +export enum Purpose { + 'assertion' = 'assertion', + 'authentication' = 'authentication', +} +export class Proof { + @ApiProperty({ + name: 'type', + description: 'signature type', + example: 'Ed25519Signature2020', + }) + @IsString() + type: string; + @ApiProperty({ + name: 'created', + description: 'Time at which documant is signed', + example: '2024-05-10T08:29:15Z', + }) + @IsString() + created: string; + @ApiProperty({ + name: 'verificationMethod', + description: 'verificationMethodId which is used for signing', + example: + 'did:hid:testnet:z6MkvfkK24wE6KxJbw6XadDkmMSZwhmtJx4mYZG6hFci9eNm#key-1', + }) + @IsString() + @ValidateVerificationMethodId() + verificationMethod: string; + @ApiProperty({ + name: 'proofPurpose', + description: 'proofPurpose', + example: SupportedPurpose.authentication, + }) + @IsString() + proofPurpose: string; + @ApiProperty({ + name: 'challenge', + description: + 'Random string used to sign in required in case of purpose authentication', + example: 'skfdhldklgjh-gaghkdhgaskda-aisgkjheyi', + }) + @ValidateIf((o) => o.purpose === 'authentication') + @IsString() + challenge?: string; + @ApiProperty({ + name: 'domain', + description: 'domain', + example: 'example.com', + }) + @ValidateIf((o) => o.purpose === 'authentication') + @IsString() + domain?: string; + + @ApiProperty({ + name: 'proofValue', + description: 'proofValue of the didDocument', + example: + 'z4CQEX1xAHauoMbAXfP3igFoKfPAETrGc3FwC5CAtnnLLZEX9FwghJ1eashf9zANfnNPYLZVyhGVg4m43Q9fs', + }) + @IsString() + proofValue: string; +} + +export class SignedDidDocument extends DidDoc { + @ApiProperty({ + name: 'proof', + description: 'proof object of didDocument', + type: Proof, + }) + proof: Proof; +} + +export class BaseDidDto { + @ApiProperty({ + name: 'didDocument', + description: 'didDocument', + type: DidDoc, + }) + didDocument: any; + @ApiProperty({ + description: 'Verification Method id for did registration', + example: 'did:hid:testnet:........#key-${idx}', + required: true, + }) + @ValidateVerificationMethodId() + @IsString() + @Matches(/^[a-zA-Z0-9\:]*testnet[a-zA-Z0-9\-:#]*$/, { + message: "Did's namespace should be testnet", + }) // this is to validate if did is generated using empty namespace + verificationMethodId: string; + @ApiProperty({ + name: 'purpose', + description: 'purpose for signing didDocument', + example: 'authentication', + required: false, + }) + @IsString() + @IsEnum(SupportedPurpose) + purpose: SupportedPurpose; + @ApiProperty({ + name: 'challenge', + description: + 'Random string used to sign in required in case of purpose authentication', + example: 'skfdhldklgjh-gaghkdhgaskda-aisgkjheyi', + required: false, + }) + @ValidateIf((o) => o.purpose === 'authentication') + @IsString() + challenge: string; + @ApiProperty({ + name: 'domain', + description: 'domain', + example: 'example.com', + required: false, + }) + @ValidateIf((o) => o.purpose === 'authentication') + @IsString() + domain: string; +} + +export class SignDidDto extends BaseDidDto { + @ApiProperty({ + name: 'didDocument', + description: 'didDocument', + type: DidDoc, + }) + @IsOptional() + @IsNotEmptyObject() + @Type(() => DidDoc) + @ValidateNested({ each: true }) + didDocument: DidDoc; + @ApiProperty({ + name: 'did', + description: 'Id of the didDocument', + example: 'did:hid:testnet:........', + }) + @IsOptional() + @IsDid() + @IsString() + did: string; +} diff --git a/src/did/dto/verify-did.dto.ts b/src/did/dto/verify-did.dto.ts new file mode 100644 index 00000000..2c0a0633 --- /dev/null +++ b/src/did/dto/verify-did.dto.ts @@ -0,0 +1,159 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { BaseDidDto, Proof, SignedDidDocument } from './sign-did.dto'; +import { + IsBoolean, + IsNotEmptyObject, + IsOptional, + IsString, + ValidateNested, +} from 'class-validator'; +import { Type } from 'class-transformer'; +import { DidDoc } from './update-did.dto'; +import { IKeyType } from 'hs-ssi-sdk'; + +export class VerifyDidDto extends BaseDidDto { + @ApiProperty({ + name: 'didDocument', + description: 'didDocument', + type: DidDoc, + }) + @IsOptional() + @IsNotEmptyObject() + @Type(() => SignedDidDocument) + @ValidateNested({ each: true }) + didDocument: SignedDidDocument; +} + +export class Controller { + @ApiProperty({ + name: '@context', + example: 'https://w3id.org/security/v2', + }) + '@context': string; + @ApiProperty({ + name: 'id', + description: 'Id of the verification method', + example: 'did:hid:testnet:....................#key-1', + }) + @IsString() + id: string; + + @ApiProperty({ + name: 'authentication', + example: ['did:hid:testnet:....................#key-1'], + description: 'Present only if purpose is authentication', + required: false, + }) + @IsString() + authentication?: Array; + @ApiProperty({ + name: 'assertionMethod', + example: ['did:hid:testnet:....................#key-1'], + description: 'Present only if purpose is assertionMethod', + required: false, + }) + @IsString() + assertionMethod?: Array; +} + +export class PurposeResult { + @ApiProperty({ + name: 'valid', + example: true, + }) + @IsBoolean() + valid: boolean; + + @ApiProperty({ + name: 'controller', + type: Controller, + }) + @ValidateNested({ each: true }) + @Type(() => Controller) + controller: Controller; +} + +export class VerificationMethod { + @ApiProperty({ + name: 'id', + description: 'Id of the verification method', + example: 'did:hid:testnet:....................#key-1', + }) + @IsString() + id: string; + @ApiProperty({ + name: 'type', + description: 'Verification key type', + example: IKeyType.Ed25519VerificationKey2020, + }) + @IsString() + type: string; + + @ApiProperty({ + name: 'publicKeyMultibase', + example: 'z6MkvfkK24wE6KxJbw6XadDkmMSZwhmtJx4mYZG6hFci9eNm', + }) + @IsString() + publicKeyMultibase: string; +} + +class VerificationProof extends Proof { + @ApiProperty({ + name: '@context', + example: [ + 'https://www.w3.org/ns/did/v1', + 'https://w3id.org/security/suites/ed25519-2020/v1', + ], + }) + '@context': Array; +} +export class Results { + @ApiProperty({ + name: 'proof', + description: '', + type: VerificationProof, + }) + @ValidateNested({ each: true }) + @Type(() => VerificationProof) + proof: VerificationProof; + @ApiProperty({ + name: 'verified', + description: 'verification result', + example: true, + }) + @IsBoolean() + verified: boolean; + @ApiProperty({ + name: 'verificationMethod', + type: VerificationMethod, + }) + @ValidateNested({ each: true }) + @Type(() => VerificationMethod) + verificationMethod: VerificationMethod; + + @ApiProperty({ + name: 'purposeResult', + type: PurposeResult, + }) + @ValidateNested({ each: true }) + @Type(() => PurposeResult) + purposeResult: PurposeResult; +} + +export class VerifyDidDocResponseDto { + @ApiProperty({ + name: 'verified', + description: 'Result of did document verification', + example: true, + // isBoolean: true + }) + @IsBoolean() + verified: boolean; + @ApiProperty({ + name: 'results', + description: 'verification result', + type: Results, + isArray: true, + }) + results: Results; +} diff --git a/src/did/services/did.service.ts b/src/did/services/did.service.ts index 0c6cf606..b96c5552 100644 --- a/src/did/services/did.service.ts +++ b/src/did/services/did.service.ts @@ -30,8 +30,9 @@ import { Did as IDidDto } from '../schemas/did.schema'; import { AddVerificationMethodDto } from '../dto/addVm.dto'; import { getAppVault, getAppMenemonic } from '../../utils/app-vault-service'; import { ConfigService } from '@nestjs/config'; +import { SignDidDto } from '../dto/sign-did.dto'; +import { VerifyDidDto } from '../dto/verify-did.dto'; import { TxSendModuleService } from 'src/tx-send-module/tx-send-module.service'; - @Injectable({ scope: Scope.REQUEST }) export class DidService { constructor( @@ -318,7 +319,7 @@ export class DidService { registerDidDto: RegisterDidDto, appDetail, ): Promise { - Logger.log('createByClientSpec() method: starts....', 'DidService'); + Logger.log('register() method: starts....', 'DidService'); let registerDidDoc; const { edvId, kmsId } = appDetail; Logger.log('register() method: initialising edv service', 'DidService'); @@ -748,4 +749,81 @@ export class DidService { } return result; } + + async SignDidDocument(signDidDto: SignDidDto, appDetail) { + Logger.log('SignDidDocument() method: starts....', 'DidService'); + const { edvId, kmsId } = appDetail; + const appMenemonic = await getAppMenemonic(kmsId); + const namespace = this.config.get('NETWORK') + ? this.config.get('NETWORK') + : 'testnet'; + const didId = signDidDto.did ? signDidDto.did : signDidDto.didDocument.id; + const DidInfo = await this.didRepositiory.findOne({ + appId: appDetail.appId, + did: didId, + }); + /** + *TODO:- check if privatekey multibase is requered to be taken from outside. + * test the case where did is and key is generated from somewhenre and api is used only for sign and verify + */ + if (!signDidDto.didDocument && DidInfo.registrationStatus != 'COMPLETED') { + throw new BadRequestException([ + 'didDocument parameter is required for private did', + ]); + } + Logger.log( + 'SignDidDocument() method: initialising didSSIService service', + 'DidService', + ); + const hypersignDid = await this.didSSIService.initiateHypersignDid( + appMenemonic, + namespace, + ); + let { didDocument } = signDidDto; + const { verificationMethodId, purpose, did, domain, challenge } = + signDidDto; + if (!didDocument) { + const didDocToBeSigned = await hypersignDid.resolve({ + did: signDidDto.did ?? did, + }); + didDocument = didDocToBeSigned.didDocument; + } + const appVault = await getAppVault(kmsId, edvId); + const { mnemonic: userMnemonic } = await appVault.getDecryptedDocument( + DidInfo.kmsId, + ); + const seed = await this.hidWallet.getSeedFromMnemonic(userMnemonic); + const { privateKeyMultibase } = await hypersignDid.generateKeys({ + seed: seed, + }); + const signedDidDocument = await hypersignDid.sign({ + didDocument: didDocument, + privateKeyMultibase, + verificationMethodId, + domain, + challenge, + purpose: purpose, + }); + return signedDidDocument; + } + async VerifyDidDocument(verifyDidDto: VerifyDidDto, appDetail) { + Logger.log('VerifyDidDocument() method: starts....', 'DidService'); + const { kmsId } = appDetail; + const appMenemonic = await getAppMenemonic(kmsId); + const namespace = this.config.get('NETWORK') + ? this.config.get('NETWORK') + : 'testnet'; + Logger.log( + 'VerifyDidDocument() method: initialising didSSIService service', + 'DidService', + ); + + const hypersignDid = await this.didSSIService.initiateHypersignDid( + appMenemonic, + namespace, + ); + const params = { ...verifyDidDto }; + const verifiedDidDocument = await hypersignDid.verify(params); + return verifiedDidDocument; + } }