Skip to content
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
286 changes: 181 additions & 105 deletions src/controllers/agent/AgentController.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
import type { AgentInfo, AgentToken, SafeW3cJsonLdVerifyCredentialOptions } from '../types'
import type {
AgentInfo,
AgentToken,
SafeW3cJsonLdVerifyCredentialOptions,
CustomW3cJsonLdSignCredentialOptions,
SignDataOptions,
VerifyDataOptions,
} from '../types'
Comment thread
coderabbitai[bot] marked this conversation as resolved.

import { JsonTransformer, W3cJsonLdVerifiableCredential } from '@credo-ts/core'
import {
JsonTransformer,
W3cJsonLdVerifiableCredential,
TypedArrayEncoder,
ClaimFormat,
W3cCredentialRecord,
DidDocument,
verkeyToPublicJwk,
getKmsKeyIdForVerifiacationMethod,
} from '@credo-ts/core'
import { Request as Req } from 'express'
import jwt from 'jsonwebtoken'
import { Controller, Get, Route, Tags, Security, Request, Post, Body } from 'tsoa'
import { Controller, Get, Route, Tags, Security, Request, Post, Body, Query } from 'tsoa'
import { injectable } from 'tsyringe'

import { AgentRole, SCOPES } from '../../enums'
import ErrorHandlingService from '../../errorHandlingService'
import { BadRequestError } from '../../errors/errors'
import { ALGORITHM_MAP } from '../../utils/constant'

@Tags('Agent')
@Route('/agent')
Expand Down Expand Up @@ -54,108 +72,166 @@
}
}

// /**
// * Delete wallet
// */
// @Security('jwt', [SCOPES.TENANT_AGENT, SCOPES.DEDICATED_AGENT])
// @Delete('/wallet')
// public async deleteWallet(@Request() request: Req) {
// try {
// const deleteWallet = await request.agent.wallet.delete()
// return deleteWallet
// } catch (error) {
// throw ErrorHandlingService.handle(error)
// }
// }

// /**
// * Verify data using a key
// *
// * @param tenantId Tenant identifier
// * @param request Verify options
// * data - Data has to be in base64 format
// * publicKeyBase58 - Public key in base58 format
// * signature - Signature in base64 format
// * @returns isValidSignature - true if signature is valid, false otherwise
// */
// @Security('jwt', [SCOPES.TENANT_AGENT, SCOPES.DEDICATED_AGENT])
// @Post('/verify')
// public async verify(@Request() request: Req, @Body() body: VerifyDataOptions) {
// try {
// assertAskarWallet(request.agent.context.wallet)
// const isValidSignature = await request.agent.context.wallet.verify({
// data: TypedArrayEncoder.fromBase64(body.data),
// key: Key.fromPublicKeyBase58(body.publicKeyBase58, body.keyType),
// signature: TypedArrayEncoder.fromBase64(body.signature),
// })
// return isValidSignature
// } catch (error) {
// throw ErrorHandlingService.handle(error)
// }
// }

// //Triage: Do we want the BW to be able to sign and verify as well?
// @Security('jwt', [SCOPES.TENANT_AGENT, SCOPES.DEDICATED_AGENT])
// @Post('/credential/sign')
// public async signCredential(
// @Request() request: Req,
// @Query('storeCredential') storeCredential: boolean,
// @Query('dataTypeToSign') dataTypeToSign: 'rawData' | 'jsonLd',
// @Body() data: CustomW3cJsonLdSignCredentialOptions | SignDataOptions | unknown,
// ) {
// try {
// // JSON-LD VC Signing
// if (dataTypeToSign === 'jsonLd') {
// const credentialData = data as unknown as W3cJsonLdSignCredentialOptions
// credentialData.format = ClaimFormat.LdpVc
// const signedCredential = (await request.agent.w3cCredentials.signCredential(
// credentialData,
// )) as W3cJsonLdVerifiableCredential
// if (storeCredential) {
// return await request.agent.w3cCredentials.storeCredential({ credential: signedCredential })
// }
// return signedCredential.toJson()
// }

// // Raw Data Signing
// const rawData = data as SignDataOptions
// if (!rawData.data) throw new BadRequestError('Missing "data" for raw data signing.')

// const hasDidOrMethod = rawData.did || rawData.method
// const hasPublicKey = rawData.publicKeyBase58 && rawData.keyType
// if (!hasDidOrMethod && !hasPublicKey) {
// throw new BadRequestError('Either (did or method) OR (publicKeyBase58 and keyType) must be provided.')
// }

// let keyToUse: Key
// if (hasDidOrMethod) {
// const dids = await request.agent.dids.getCreatedDids({
// method: rawData.method || undefined,
// did: rawData.did || undefined,
// })
// const verificationMethod = dids[0]?.didDocument?.verificationMethod?.[0]?.publicKeyBase58
// if (!verificationMethod) {
// throw new BadRequestError('No publicKeyBase58 found for the given DID or method.')
// }
// keyToUse = Key.fromPublicKeyBase58(verificationMethod, rawData.keyType)
// } else {
// keyToUse = Key.fromPublicKeyBase58(rawData.publicKeyBase58, rawData.keyType)
// }

// if (!keyToUse) {
// throw new Error('Unable to construct signing key. ')
// }

// const signature = await request.agent.context.wallet.sign({
// data: TypedArrayEncoder.fromBase64(rawData.data),
// key: keyToUse,
// })

// return TypedArrayEncoder.toBase64(signature)
// } catch (error) {
// throw ErrorHandlingService.handle(error)
// }
// }
/**
* Verify data using a key
*
* @param body Verify options
* data - Data has to be in base64 format
* publicKeyBase58 - Public key in base58 format
* signature - Signature in base64 format
* @returns isValidSignature - true if signature is valid, false otherwise
*/
@Security('jwt', [SCOPES.TENANT_AGENT, SCOPES.DEDICATED_AGENT])
@Post('/verify')
public async verify(@Request() request: Req, @Body() body: VerifyDataOptions): Promise<{ verified: boolean }> {
try {
if (!body.data || !body.signature || !body.publicKeyBase58 || !body.keyType) {
throw new BadRequestError(
'Missing required fields: data, signature, publicKeyBase58, and keyType are required.',
)
}

// Convert verkey to JWK
const publicJwkWrapper = verkeyToPublicJwk(body.publicKeyBase58)
const publicJwk = (publicJwkWrapper as any).jwk?.jwk ?? (publicJwkWrapper as any).jwk ?? publicJwkWrapper

Check warning on line 96 in src/controllers/agent/AgentController.ts

View workflow job for this annotation

GitHub Actions / Validate

Unexpected any. Specify a different type

Check warning on line 96 in src/controllers/agent/AgentController.ts

View workflow job for this annotation

GitHub Actions / Validate

Unexpected any. Specify a different type

const result = await request.agent.kms.verify({
data: TypedArrayEncoder.fromBase64(body.data),
signature: TypedArrayEncoder.fromBase64(body.signature),
key: {
publicJwk: publicJwk as any,

Check warning on line 102 in src/controllers/agent/AgentController.ts

View workflow job for this annotation

GitHub Actions / Validate

Unexpected any. Specify a different type
},
algorithm: (ALGORITHM_MAP[body.keyType.toLowerCase()] || body.keyType) as any,

Check warning on line 104 in src/controllers/agent/AgentController.ts

View workflow job for this annotation

GitHub Actions / Validate

Unexpected any. Specify a different type
})

return { verified: result.verified }
} catch (error) {
throw ErrorHandlingService.handle(error)
}
}

/**
* Sign credential or raw data
*/
@Security('jwt', [SCOPES.TENANT_AGENT, SCOPES.DEDICATED_AGENT])
@Post('/credential/sign')
public async signCredential(
@Request() request: Req,
@Query('storeCredential') storeCredential: boolean,
@Query('dataTypeToSign') dataTypeToSign: 'rawData' | 'jsonLd',
@Body() body: CustomW3cJsonLdSignCredentialOptions | SignDataOptions,
) {
try {
const typeToSign = (dataTypeToSign || 'rawData').toLowerCase()
request.agent.config.logger.info(
`[SignCredential] dataTypeToSign: ${dataTypeToSign}, typeToSign: ${typeToSign}, storeCredential: ${storeCredential}`,
)

if (typeToSign === 'jsonld') {
return await this.signJsonLd(request, body as CustomW3cJsonLdSignCredentialOptions, storeCredential)

Check warning on line 131 in src/controllers/agent/AgentController.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

This assertion is unnecessary since it does not change the type of the expression.

See more on https://sonarcloud.io/project/issues?id=credebl_afj-controller&issues=AZ4hkmCSDNGSfvvKOB1T&open=AZ4hkmCSDNGSfvvKOB1T&pullRequest=384
}

return await this.signRawData(request, body as SignDataOptions)
} catch (error) {
const err = error as any

Check warning on line 136 in src/controllers/agent/AgentController.ts

View workflow job for this annotation

GitHub Actions / Validate

Unexpected any. Specify a different type
request.agent.config.logger.error(`[SignCredential] Error: ${err.message}`, { stack: err.stack })
throw ErrorHandlingService.handle(error)
}
}

private async signJsonLd(request: Req, body: CustomW3cJsonLdSignCredentialOptions, storeCredential: boolean) {
const credentialData = body as any

Check warning on line 143 in src/controllers/agent/AgentController.ts

View workflow job for this annotation

GitHub Actions / Validate

Unexpected any. Specify a different type

// Ensure signerOptions is populated if top-level fields are provided
if (!credentialData.signerOptions && (credentialData.verificationMethod || credentialData.proofType)) {
credentialData.signerOptions = {
verificationMethod: credentialData.verificationMethod,
type: credentialData.proofType,
method: 'did', // default to did if not specified
}
}

credentialData.format = ClaimFormat.LdpVc
const signedCredential = (await request.agent.w3cCredentials.signCredential(
credentialData,
)) as W3cJsonLdVerifiableCredential

if (storeCredential) {
const record = W3cCredentialRecord.fromCredential(signedCredential)
return await request.agent.w3cCredentials.store({ record })
}
return signedCredential.toJson()
}

private async signRawData(request: Req, body: SignDataOptions) {
if (!body.data) throw new BadRequestError('Missing "data" for raw data signing.')

const kmsKeyId = await this.resolveKmsKeyId(request, body)

const signature = await request.agent.kms.sign({
data: TypedArrayEncoder.fromBase64(body.data),
keyId: kmsKeyId as string,
algorithm: (body.keyType ? ALGORITHM_MAP[body.keyType.toLowerCase()] || body.keyType : 'EdDSA') as any,

Check warning on line 174 in src/controllers/agent/AgentController.ts

View workflow job for this annotation

GitHub Actions / Validate

Unexpected any. Specify a different type
})

return TypedArrayEncoder.toBase64(signature.signature)
}

private async resolveKmsKeyId(request: Req, body: SignDataOptions): Promise<string> {

Check failure on line 180 in src/controllers/agent/AgentController.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 16 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=credebl_afj-controller&issues=AZ4hkmCSDNGSfvvKOB1U&open=AZ4hkmCSDNGSfvvKOB1U&pullRequest=384
const hasDidOrMethod = body.did || body.method
const hasPublicKey = body.publicKeyBase58 && body.keyType
if (!hasDidOrMethod && !hasPublicKey) {
throw new BadRequestError('Either (did or method) OR (publicKeyBase58 and keyType) must be provided.')
}

if (!hasDidOrMethod) {
return body.publicKeyBase58
}

let kmsKeyId: string | undefined = undefined
let didDocument: DidDocument | undefined | null = undefined

const dids = await request.agent.dids.getCreatedDids({
method: body.method || undefined,
did: body.did || undefined,
})

const didRecord = dids[0]
if (didRecord) {
didDocument = didRecord.didDocument
if (didRecord.keys && didRecord.keys.length > 0) {
kmsKeyId = didRecord.keys[0].kmsKeyId
}
}

if (!didDocument && body.did) {
const resolution = await request.agent.dids.resolve(body.did)
didDocument = resolution.didDocument
}

if (!didDocument) {
throw new BadRequestError('No DID document found.')
}

if (!kmsKeyId) {
const verificationMethod = didDocument.verificationMethod?.[0]
if (!verificationMethod) {
throw new BadRequestError('No verification method found on DID document.')
}

// Try multiple ways to get the kmsKeyId
const derivedKeyId = getKmsKeyIdForVerifiacationMethod(verificationMethod)
const publicKeyBase58 = (verificationMethod as any).publicKeyBase58

Check warning on line 224 in src/controllers/agent/AgentController.ts

View workflow job for this annotation

GitHub Actions / Validate

Unexpected any. Specify a different type
const vmId = verificationMethod.id || ''
const idPart = vmId.includes('#') ? vmId.split('#')[1] : undefined

kmsKeyId = (derivedKeyId || publicKeyBase58 || idPart || vmId) as string

request.agent.config.logger.info(`[SignCredential] Resolved kmsKeyId via fallback: ${kmsKeyId}`)
}

return kmsKeyId
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@Security('jwt', [SCOPES.TENANT_AGENT, SCOPES.DEDICATED_AGENT])
@Post('/credential/verify')
Expand Down
6 changes: 5 additions & 1 deletion src/controllers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,11 @@ export type ExtensibleW3cCredential = W3cCredential & {
credentialSubject: SingleOrArray<ExtensibleW3cCredentialSubject>
}

export type CustomW3cJsonLdSignCredentialOptions = Omit<W3cJsonLdSignCredentialOptions, 'format'> & {
export type CustomW3cJsonLdSignCredentialOptions = Omit<W3cJsonLdSignCredentialOptions, 'format' | 'credential'> & {
credential: Omit<W3cCredential, 'context' | 'credentialSubject'> & {
'@context': Array<string | Record<string, unknown>>
credentialSubject: SingleOrArray<JsonObject>
}
[key: string]: unknown
}

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand Down
Loading
Loading