|
1 | | -import type { AgentInfo, AgentToken, SafeW3cJsonLdVerifyCredentialOptions } from '../types' |
| 1 | +import type { |
| 2 | + AgentInfo, |
| 3 | + AgentToken, |
| 4 | + SafeW3cJsonLdVerifyCredentialOptions, |
| 5 | + CustomW3cJsonLdSignCredentialOptions, |
| 6 | + SignDataOptions, |
| 7 | + VerifyDataOptions, |
| 8 | +} from '../types' |
2 | 9 |
|
3 | | -import { JsonTransformer, W3cJsonLdVerifiableCredential } from '@credo-ts/core' |
| 10 | +import { |
| 11 | + JsonTransformer, |
| 12 | + W3cJsonLdVerifiableCredential, |
| 13 | + TypedArrayEncoder, |
| 14 | + ClaimFormat, |
| 15 | + W3cCredentialRecord, |
| 16 | + DidDocument, |
| 17 | + verkeyToPublicJwk, |
| 18 | + getKmsKeyIdForVerifiacationMethod, |
| 19 | +} from '@credo-ts/core' |
4 | 20 | import { Request as Req } from 'express' |
5 | 21 | import jwt from 'jsonwebtoken' |
6 | | -import { Controller, Get, Route, Tags, Security, Request, Post, Body } from 'tsoa' |
| 22 | +import { Controller, Get, Route, Tags, Security, Request, Post, Body, Query } from 'tsoa' |
7 | 23 | import { injectable } from 'tsyringe' |
8 | 24 |
|
9 | 25 | import { AgentRole, SCOPES } from '../../enums' |
10 | 26 | import ErrorHandlingService from '../../errorHandlingService' |
| 27 | +import { BadRequestError } from '../../errors/errors' |
| 28 | +import { ALGORITHM_MAP } from '../../utils/constant' |
11 | 29 |
|
12 | 30 | @Tags('Agent') |
13 | 31 | @Route('/agent') |
@@ -54,108 +72,166 @@ export class AgentController extends Controller { |
54 | 72 | } |
55 | 73 | } |
56 | 74 |
|
57 | | - // /** |
58 | | - // * Delete wallet |
59 | | - // */ |
60 | | - // @Security('jwt', [SCOPES.TENANT_AGENT, SCOPES.DEDICATED_AGENT]) |
61 | | - // @Delete('/wallet') |
62 | | - // public async deleteWallet(@Request() request: Req) { |
63 | | - // try { |
64 | | - // const deleteWallet = await request.agent.wallet.delete() |
65 | | - // return deleteWallet |
66 | | - // } catch (error) { |
67 | | - // throw ErrorHandlingService.handle(error) |
68 | | - // } |
69 | | - // } |
70 | | - |
71 | | - // /** |
72 | | - // * Verify data using a key |
73 | | - // * |
74 | | - // * @param tenantId Tenant identifier |
75 | | - // * @param request Verify options |
76 | | - // * data - Data has to be in base64 format |
77 | | - // * publicKeyBase58 - Public key in base58 format |
78 | | - // * signature - Signature in base64 format |
79 | | - // * @returns isValidSignature - true if signature is valid, false otherwise |
80 | | - // */ |
81 | | - // @Security('jwt', [SCOPES.TENANT_AGENT, SCOPES.DEDICATED_AGENT]) |
82 | | - // @Post('/verify') |
83 | | - // public async verify(@Request() request: Req, @Body() body: VerifyDataOptions) { |
84 | | - // try { |
85 | | - // assertAskarWallet(request.agent.context.wallet) |
86 | | - // const isValidSignature = await request.agent.context.wallet.verify({ |
87 | | - // data: TypedArrayEncoder.fromBase64(body.data), |
88 | | - // key: Key.fromPublicKeyBase58(body.publicKeyBase58, body.keyType), |
89 | | - // signature: TypedArrayEncoder.fromBase64(body.signature), |
90 | | - // }) |
91 | | - // return isValidSignature |
92 | | - // } catch (error) { |
93 | | - // throw ErrorHandlingService.handle(error) |
94 | | - // } |
95 | | - // } |
96 | | - |
97 | | - // //Triage: Do we want the BW to be able to sign and verify as well? |
98 | | - // @Security('jwt', [SCOPES.TENANT_AGENT, SCOPES.DEDICATED_AGENT]) |
99 | | - // @Post('/credential/sign') |
100 | | - // public async signCredential( |
101 | | - // @Request() request: Req, |
102 | | - // @Query('storeCredential') storeCredential: boolean, |
103 | | - // @Query('dataTypeToSign') dataTypeToSign: 'rawData' | 'jsonLd', |
104 | | - // @Body() data: CustomW3cJsonLdSignCredentialOptions | SignDataOptions | unknown, |
105 | | - // ) { |
106 | | - // try { |
107 | | - // // JSON-LD VC Signing |
108 | | - // if (dataTypeToSign === 'jsonLd') { |
109 | | - // const credentialData = data as unknown as W3cJsonLdSignCredentialOptions |
110 | | - // credentialData.format = ClaimFormat.LdpVc |
111 | | - // const signedCredential = (await request.agent.w3cCredentials.signCredential( |
112 | | - // credentialData, |
113 | | - // )) as W3cJsonLdVerifiableCredential |
114 | | - // if (storeCredential) { |
115 | | - // return await request.agent.w3cCredentials.storeCredential({ credential: signedCredential }) |
116 | | - // } |
117 | | - // return signedCredential.toJson() |
118 | | - // } |
119 | | - |
120 | | - // // Raw Data Signing |
121 | | - // const rawData = data as SignDataOptions |
122 | | - // if (!rawData.data) throw new BadRequestError('Missing "data" for raw data signing.') |
123 | | - |
124 | | - // const hasDidOrMethod = rawData.did || rawData.method |
125 | | - // const hasPublicKey = rawData.publicKeyBase58 && rawData.keyType |
126 | | - // if (!hasDidOrMethod && !hasPublicKey) { |
127 | | - // throw new BadRequestError('Either (did or method) OR (publicKeyBase58 and keyType) must be provided.') |
128 | | - // } |
129 | | - |
130 | | - // let keyToUse: Key |
131 | | - // if (hasDidOrMethod) { |
132 | | - // const dids = await request.agent.dids.getCreatedDids({ |
133 | | - // method: rawData.method || undefined, |
134 | | - // did: rawData.did || undefined, |
135 | | - // }) |
136 | | - // const verificationMethod = dids[0]?.didDocument?.verificationMethod?.[0]?.publicKeyBase58 |
137 | | - // if (!verificationMethod) { |
138 | | - // throw new BadRequestError('No publicKeyBase58 found for the given DID or method.') |
139 | | - // } |
140 | | - // keyToUse = Key.fromPublicKeyBase58(verificationMethod, rawData.keyType) |
141 | | - // } else { |
142 | | - // keyToUse = Key.fromPublicKeyBase58(rawData.publicKeyBase58, rawData.keyType) |
143 | | - // } |
144 | | - |
145 | | - // if (!keyToUse) { |
146 | | - // throw new Error('Unable to construct signing key. ') |
147 | | - // } |
148 | | - |
149 | | - // const signature = await request.agent.context.wallet.sign({ |
150 | | - // data: TypedArrayEncoder.fromBase64(rawData.data), |
151 | | - // key: keyToUse, |
152 | | - // }) |
153 | | - |
154 | | - // return TypedArrayEncoder.toBase64(signature) |
155 | | - // } catch (error) { |
156 | | - // throw ErrorHandlingService.handle(error) |
157 | | - // } |
158 | | - // } |
| 75 | + /** |
| 76 | + * Verify data using a key |
| 77 | + * |
| 78 | + * @param body Verify options |
| 79 | + * data - Data has to be in base64 format |
| 80 | + * publicKeyBase58 - Public key in base58 format |
| 81 | + * signature - Signature in base64 format |
| 82 | + * @returns isValidSignature - true if signature is valid, false otherwise |
| 83 | + */ |
| 84 | + @Security('jwt', [SCOPES.TENANT_AGENT, SCOPES.DEDICATED_AGENT]) |
| 85 | + @Post('/verify') |
| 86 | + public async verify(@Request() request: Req, @Body() body: VerifyDataOptions): Promise<{ verified: boolean }> { |
| 87 | + try { |
| 88 | + if (!body.data || !body.signature || !body.publicKeyBase58 || !body.keyType) { |
| 89 | + throw new BadRequestError( |
| 90 | + 'Missing required fields: data, signature, publicKeyBase58, and keyType are required.', |
| 91 | + ) |
| 92 | + } |
| 93 | + |
| 94 | + // Convert verkey to JWK |
| 95 | + const publicJwkWrapper = verkeyToPublicJwk(body.publicKeyBase58) |
| 96 | + const publicJwk = (publicJwkWrapper as any).jwk?.jwk ?? (publicJwkWrapper as any).jwk ?? publicJwkWrapper |
| 97 | + |
| 98 | + const result = await request.agent.kms.verify({ |
| 99 | + data: TypedArrayEncoder.fromBase64(body.data), |
| 100 | + signature: TypedArrayEncoder.fromBase64(body.signature), |
| 101 | + key: { |
| 102 | + publicJwk: publicJwk as any, |
| 103 | + }, |
| 104 | + algorithm: (ALGORITHM_MAP[body.keyType.toLowerCase()] || body.keyType) as any, |
| 105 | + }) |
| 106 | + |
| 107 | + return { verified: result.verified } |
| 108 | + } catch (error) { |
| 109 | + throw ErrorHandlingService.handle(error) |
| 110 | + } |
| 111 | + } |
| 112 | + |
| 113 | + /** |
| 114 | + * Sign credential or raw data |
| 115 | + */ |
| 116 | + @Security('jwt', [SCOPES.TENANT_AGENT, SCOPES.DEDICATED_AGENT]) |
| 117 | + @Post('/credential/sign') |
| 118 | + public async signCredential( |
| 119 | + @Request() request: Req, |
| 120 | + @Query('storeCredential') storeCredential: boolean, |
| 121 | + @Query('dataTypeToSign') dataTypeToSign: 'rawData' | 'jsonLd', |
| 122 | + @Body() body: CustomW3cJsonLdSignCredentialOptions | SignDataOptions, |
| 123 | + ) { |
| 124 | + try { |
| 125 | + const typeToSign = (dataTypeToSign || 'rawData').toLowerCase() |
| 126 | + request.agent.config.logger.info( |
| 127 | + `[SignCredential] dataTypeToSign: ${dataTypeToSign}, typeToSign: ${typeToSign}, storeCredential: ${storeCredential}`, |
| 128 | + ) |
| 129 | + |
| 130 | + if (typeToSign === 'jsonld') { |
| 131 | + return await this.signJsonLd(request, body as CustomW3cJsonLdSignCredentialOptions, storeCredential) |
| 132 | + } |
| 133 | + |
| 134 | + return await this.signRawData(request, body as SignDataOptions) |
| 135 | + } catch (error) { |
| 136 | + const err = error as any |
| 137 | + request.agent.config.logger.error(`[SignCredential] Error: ${err.message}`, { stack: err.stack }) |
| 138 | + throw ErrorHandlingService.handle(error) |
| 139 | + } |
| 140 | + } |
| 141 | + |
| 142 | + private async signJsonLd(request: Req, body: CustomW3cJsonLdSignCredentialOptions, storeCredential: boolean) { |
| 143 | + const credentialData = body as any |
| 144 | + |
| 145 | + // Ensure signerOptions is populated if top-level fields are provided |
| 146 | + if (!credentialData.signerOptions && (credentialData.verificationMethod || credentialData.proofType)) { |
| 147 | + credentialData.signerOptions = { |
| 148 | + verificationMethod: credentialData.verificationMethod, |
| 149 | + type: credentialData.proofType, |
| 150 | + method: 'did', // default to did if not specified |
| 151 | + } |
| 152 | + } |
| 153 | + |
| 154 | + credentialData.format = ClaimFormat.LdpVc |
| 155 | + const signedCredential = (await request.agent.w3cCredentials.signCredential( |
| 156 | + credentialData, |
| 157 | + )) as W3cJsonLdVerifiableCredential |
| 158 | + |
| 159 | + if (storeCredential) { |
| 160 | + const record = W3cCredentialRecord.fromCredential(signedCredential) |
| 161 | + return await request.agent.w3cCredentials.store({ record }) |
| 162 | + } |
| 163 | + return signedCredential.toJson() |
| 164 | + } |
| 165 | + |
| 166 | + private async signRawData(request: Req, body: SignDataOptions) { |
| 167 | + if (!body.data) throw new BadRequestError('Missing "data" for raw data signing.') |
| 168 | + |
| 169 | + const kmsKeyId = await this.resolveKmsKeyId(request, body) |
| 170 | + |
| 171 | + const signature = await request.agent.kms.sign({ |
| 172 | + data: TypedArrayEncoder.fromBase64(body.data), |
| 173 | + keyId: kmsKeyId as string, |
| 174 | + algorithm: (body.keyType ? ALGORITHM_MAP[body.keyType.toLowerCase()] || body.keyType : 'EdDSA') as any, |
| 175 | + }) |
| 176 | + |
| 177 | + return TypedArrayEncoder.toBase64(signature.signature) |
| 178 | + } |
| 179 | + |
| 180 | + private async resolveKmsKeyId(request: Req, body: SignDataOptions): Promise<string> { |
| 181 | + const hasDidOrMethod = body.did || body.method |
| 182 | + const hasPublicKey = body.publicKeyBase58 && body.keyType |
| 183 | + if (!hasDidOrMethod && !hasPublicKey) { |
| 184 | + throw new BadRequestError('Either (did or method) OR (publicKeyBase58 and keyType) must be provided.') |
| 185 | + } |
| 186 | + |
| 187 | + if (!hasDidOrMethod) { |
| 188 | + return body.publicKeyBase58 |
| 189 | + } |
| 190 | + |
| 191 | + let kmsKeyId: string | undefined = undefined |
| 192 | + let didDocument: DidDocument | undefined | null = undefined |
| 193 | + |
| 194 | + const dids = await request.agent.dids.getCreatedDids({ |
| 195 | + method: body.method || undefined, |
| 196 | + did: body.did || undefined, |
| 197 | + }) |
| 198 | + |
| 199 | + const didRecord = dids[0] |
| 200 | + if (didRecord) { |
| 201 | + didDocument = didRecord.didDocument |
| 202 | + if (didRecord.keys && didRecord.keys.length > 0) { |
| 203 | + kmsKeyId = didRecord.keys[0].kmsKeyId |
| 204 | + } |
| 205 | + } |
| 206 | + |
| 207 | + if (!didDocument && body.did) { |
| 208 | + const resolution = await request.agent.dids.resolve(body.did) |
| 209 | + didDocument = resolution.didDocument |
| 210 | + } |
| 211 | + |
| 212 | + if (!didDocument) { |
| 213 | + throw new BadRequestError('No DID document found.') |
| 214 | + } |
| 215 | + |
| 216 | + if (!kmsKeyId) { |
| 217 | + const verificationMethod = didDocument.verificationMethod?.[0] |
| 218 | + if (!verificationMethod) { |
| 219 | + throw new BadRequestError('No verification method found on DID document.') |
| 220 | + } |
| 221 | + |
| 222 | + // Try multiple ways to get the kmsKeyId |
| 223 | + const derivedKeyId = getKmsKeyIdForVerifiacationMethod(verificationMethod) |
| 224 | + const publicKeyBase58 = (verificationMethod as any).publicKeyBase58 |
| 225 | + const vmId = verificationMethod.id || '' |
| 226 | + const idPart = vmId.includes('#') ? vmId.split('#')[1] : undefined |
| 227 | + |
| 228 | + kmsKeyId = (derivedKeyId || publicKeyBase58 || idPart || vmId) as string |
| 229 | + |
| 230 | + request.agent.config.logger.info(`[SignCredential] Resolved kmsKeyId via fallback: ${kmsKeyId}`) |
| 231 | + } |
| 232 | + |
| 233 | + return kmsKeyId |
| 234 | + } |
159 | 235 |
|
160 | 236 | @Security('jwt', [SCOPES.TENANT_AGENT, SCOPES.DEDICATED_AGENT]) |
161 | 237 | @Post('/credential/verify') |
|
0 commit comments