Skip to content

Commit 3116c1a

Browse files
authored
Merge pull request #384 from credebl/feat/credentials-sign-verify
feat: credentials sign & verify
2 parents ca1d777 + fcf9592 commit 3116c1a

5 files changed

Lines changed: 764 additions & 138 deletions

File tree

src/controllers/agent/AgentController.ts

Lines changed: 181 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,31 @@
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'
29

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'
420
import { Request as Req } from 'express'
521
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'
723
import { injectable } from 'tsyringe'
824

925
import { AgentRole, SCOPES } from '../../enums'
1026
import ErrorHandlingService from '../../errorHandlingService'
27+
import { BadRequestError } from '../../errors/errors'
28+
import { ALGORITHM_MAP } from '../../utils/constant'
1129

1230
@Tags('Agent')
1331
@Route('/agent')
@@ -54,108 +72,166 @@ export class AgentController extends Controller {
5472
}
5573
}
5674

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+
}
159235

160236
@Security('jwt', [SCOPES.TENANT_AGENT, SCOPES.DEDICATED_AGENT])
161237
@Post('/credential/verify')

src/controllers/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -464,7 +464,11 @@ export type ExtensibleW3cCredential = W3cCredential & {
464464
credentialSubject: SingleOrArray<ExtensibleW3cCredentialSubject>
465465
}
466466

467-
export type CustomW3cJsonLdSignCredentialOptions = Omit<W3cJsonLdSignCredentialOptions, 'format'> & {
467+
export type CustomW3cJsonLdSignCredentialOptions = Omit<W3cJsonLdSignCredentialOptions, 'format' | 'credential'> & {
468+
credential: Omit<W3cCredential, 'context' | 'credentialSubject'> & {
469+
'@context': Array<string | Record<string, unknown>>
470+
credentialSubject: SingleOrArray<JsonObject>
471+
}
468472
[key: string]: unknown
469473
}
470474

0 commit comments

Comments
 (0)