@sphereon/ssi-sdk.credential-vcdm 0.33.1-feature.jose.vcdm.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,209 @@
1
+ import type { IAgentPlugin, IIdentifier, IVerifyResult, VerifiableCredential, VerifiablePresentation } from '@veramo/core'
2
+ import { schema } from '@veramo/core'
3
+
4
+ import type {
5
+ ICreateVerifiableCredentialLDArgs,
6
+ ICreateVerifiablePresentationLDArgs,
7
+ IVcdmCredentialPlugin,
8
+ IVcdmCredentialProvider,
9
+ IVcdmIssuerAgentContext,
10
+ IVcdmVerifierAgentContext,
11
+ IVerifyCredentialLDArgs,
12
+ IVerifyPresentationLDArgs,
13
+ } from './types'
14
+
15
+ import { isDefined, MANDATORY_CREDENTIAL_CONTEXT, processEntryToArray } from '@veramo/utils'
16
+ import Debug from 'debug'
17
+ import { extractIssuer, isRevoked } from './functions'
18
+ import type { W3CVerifiableCredential, W3CVerifiablePresentation } from '@sphereon/ssi-types'
19
+ import type { VerifiableCredentialSP, VerifiablePresentationSP } from '@sphereon/ssi-sdk.core'
20
+
21
+ const debug = Debug('sphereon:ssi-sdk:vcdm')
22
+
23
+ /**
24
+ * A plugin that implements the {@link @sphereon/ssi-sdk.credential-vcdm#IVcdmCredentialPlugin} methods.
25
+ *
26
+ * @public
27
+ */
28
+ export class VcdmCredentialPlugin implements IAgentPlugin {
29
+ readonly methods: IVcdmCredentialPlugin
30
+ readonly schema = {
31
+ components: {
32
+ schemas: {
33
+ ...schema.ICredentialIssuer.components.schemas,
34
+ ...schema.ICredentialVerifier.components.schemas,
35
+ },
36
+ methods: {
37
+ ...schema.ICredentialIssuer.components.methods,
38
+ ...schema.ICredentialVerifier.components.methods,
39
+ },
40
+ },
41
+ }
42
+ private issuers: IVcdmCredentialProvider[]
43
+
44
+ constructor(options: { issuers: IVcdmCredentialProvider[] }) {
45
+ this.issuers = options.issuers
46
+ this.methods = {
47
+ listUsableProofFormats: this.listUsableProofFormats.bind(this),
48
+ createVerifiableCredential: this.createVerifiableCredential.bind(this),
49
+ verifyCredential: this.verifyCredential.bind(this),
50
+ createVerifiablePresentation: this.createVerifiablePresentation.bind(this),
51
+ verifyPresentation: this.verifyPresentation.bind(this),
52
+ }
53
+ }
54
+
55
+ async listUsableProofFormats(did: IIdentifier, context: IVcdmIssuerAgentContext): Promise<string[]> {
56
+ const signingOptions: string[] = []
57
+ const keys = did.keys
58
+ for (const key of keys) {
59
+ for (const issuer of this.issuers) {
60
+ if (issuer.matchKeyForType(key)) {
61
+ signingOptions.push(issuer.getTypeProofFormat())
62
+ }
63
+ }
64
+ }
65
+ return signingOptions
66
+ }
67
+
68
+ /** {@inheritdoc @veramo/core#ICredentialIssuer.createVerifiableCredential} */
69
+ async createVerifiableCredential(args: ICreateVerifiableCredentialLDArgs, context: IVcdmIssuerAgentContext): Promise<VerifiableCredentialSP> {
70
+ let { credential, proofFormat, /* keyRef, removeOriginalFields,*/ now /*, ...otherOptions */ } = args
71
+ const credentialContext = processEntryToArray(credential['@context'], MANDATORY_CREDENTIAL_CONTEXT)
72
+ const credentialType = processEntryToArray(credential.type, 'VerifiableCredential')
73
+
74
+ // only add issuanceDate for JWT
75
+ now = typeof now === 'number' ? new Date(now * 1000) : now
76
+ if (!Object.getOwnPropertyNames(credential).includes('issuanceDate')) {
77
+ credential.issuanceDate = (now instanceof Date ? now : new Date()).toISOString()
78
+ }
79
+
80
+ credential = {
81
+ ...credential,
82
+ '@context': credentialContext,
83
+ type: credentialType,
84
+ }
85
+
86
+ //FIXME: if the identifier is not found, the error message should reflect that.
87
+ const issuer = extractIssuer(credential, { removeParameters: true })
88
+ if (!issuer || typeof issuer === 'undefined') {
89
+ throw new Error('invalid_argument: credential.issuer must not be empty')
90
+ }
91
+
92
+ try {
93
+ await context.agent.didManagerGet({ did: issuer })
94
+ } catch (e) {
95
+ throw new Error(`invalid_argument: credential.issuer must be a DID managed by this agent. ${e}`)
96
+ }
97
+ try {
98
+ async function findAndIssueCredential(issuers: IVcdmCredentialProvider[]) {
99
+ for (const issuer of issuers) {
100
+ if (issuer.canIssueCredentialType({ proofFormat })) {
101
+ return await issuer.createVerifiableCredential(args, context)
102
+ }
103
+ }
104
+ throw new Error('invalid_setup: No issuer found for the requested proof format')
105
+ }
106
+ const verifiableCredential = await findAndIssueCredential(this.issuers)
107
+ return verifiableCredential
108
+ } catch (error) {
109
+ debug(error)
110
+ return Promise.reject(error)
111
+ }
112
+ }
113
+
114
+ /** {@inheritdoc @veramo/core#ICredentialVerifier.verifyCredential} */
115
+ async verifyCredential(args: IVerifyCredentialLDArgs, context: IVcdmVerifierAgentContext): Promise<IVerifyResult> {
116
+ let { credential, policies /*, ...otherOptions*/ } = args
117
+ let verifiedCredential: VerifiableCredential
118
+ let verificationResult: IVerifyResult | undefined = { verified: false }
119
+
120
+ async function findAndVerifyCredential(issuers: IVcdmCredentialProvider[]): Promise<IVerifyResult> {
121
+ for (const issuer of issuers) {
122
+ if (issuer.canVerifyDocumentType({ document: credential as W3CVerifiableCredential })) {
123
+ return issuer.verifyCredential(args, context)
124
+ }
125
+ }
126
+ return Promise.reject(Error('invalid_setup: No issuer found for the provided credential'))
127
+ }
128
+ verificationResult = await findAndVerifyCredential(this.issuers)
129
+ verifiedCredential = <VerifiableCredential>credential
130
+
131
+ if (policies?.credentialStatus !== false && (await isRevoked(verifiedCredential, context as any))) {
132
+ verificationResult = {
133
+ verified: false,
134
+ error: {
135
+ message: 'revoked: The credential was revoked by the issuer',
136
+ errorCode: 'revoked',
137
+ },
138
+ }
139
+ }
140
+
141
+ return verificationResult
142
+ }
143
+
144
+ /** {@inheritdoc @veramo/core#ICredentialIssuer.createVerifiablePresentation} */
145
+ async createVerifiablePresentation(args: ICreateVerifiablePresentationLDArgs, context: IVcdmIssuerAgentContext): Promise<VerifiablePresentationSP> {
146
+ let {
147
+ presentation,
148
+ proofFormat,
149
+ /* domain,
150
+ challenge,
151
+ removeOriginalFields,
152
+ keyRef,*/
153
+ // save,
154
+ /*now,*/
155
+ /*...otherOptions*/
156
+ } = args
157
+ const presentationContext: string[] = processEntryToArray(args?.presentation?.['@context'], MANDATORY_CREDENTIAL_CONTEXT)
158
+ const presentationType = processEntryToArray(args?.presentation?.type, 'VerifiablePresentation')
159
+ presentation = {
160
+ ...presentation,
161
+ '@context': presentationContext,
162
+ type: presentationType,
163
+ }
164
+
165
+ if (!isDefined(presentation.holder)) {
166
+ throw new Error('invalid_argument: presentation.holder must not be empty')
167
+ }
168
+
169
+ if (presentation.verifiableCredential) {
170
+ presentation.verifiableCredential = presentation.verifiableCredential.map((cred) => {
171
+ // map JWT credentials to their canonical form
172
+ if (typeof cred !== 'string' && cred.proof.jwt) {
173
+ return cred.proof.jwt
174
+ } else {
175
+ return cred
176
+ }
177
+ })
178
+ }
179
+
180
+ let verifiablePresentation: VerifiablePresentation | undefined
181
+
182
+ async function findAndCreatePresentation(issuers: IVcdmCredentialProvider[]) {
183
+ for (const issuer of issuers) {
184
+ if (issuer.canIssueCredentialType({ proofFormat })) {
185
+ return await issuer.createVerifiablePresentation(args, context)
186
+ }
187
+ }
188
+ throw new Error('invalid_setup: No issuer found for the requested proof format')
189
+ }
190
+
191
+ verifiablePresentation = await findAndCreatePresentation(this.issuers)
192
+ return verifiablePresentation as VerifiablePresentationSP // fixme: this is a hack to get around the fact that the return type is not correct.
193
+ }
194
+
195
+ /** {@inheritdoc @veramo/core#ICredentialVerifier.verifyPresentation} */
196
+ async verifyPresentation(args: IVerifyPresentationLDArgs, context: IVcdmVerifierAgentContext): Promise<IVerifyResult> {
197
+ let { presentation /*domain, challenge, fetchRemoteContexts, policies, ...otherOptions*/ } = args
198
+ async function findAndVerifyPresentation(issuers: IVcdmCredentialProvider[]): Promise<IVerifyResult> {
199
+ for (const issuer of issuers) {
200
+ if (issuer.canVerifyDocumentType({ document: presentation as W3CVerifiablePresentation })) {
201
+ return issuer.verifyPresentation(args, context)
202
+ }
203
+ }
204
+ throw new Error('invalid_setup: No verifier found for the provided presentation')
205
+ }
206
+ const result = await findAndVerifyPresentation(this.issuers)
207
+ return result
208
+ }
209
+ }
@@ -0,0 +1,96 @@
1
+ import {
2
+ CredentialPayload,
3
+ IAgentContext, ICredentialStatusVerifier,
4
+ IIdentifier,
5
+ IKey,
6
+ IssuerType,
7
+ PresentationPayload,
8
+ VerifiableCredential,
9
+ W3CVerifiableCredential,
10
+ W3CVerifiablePresentation
11
+ } from '@veramo/core'
12
+ import { isDefined } from '@veramo/utils'
13
+ import { decodeJWT } from 'did-jwt'
14
+
15
+ /**
16
+ * Decodes a credential or presentation and returns the issuer ID
17
+ * `iss` from a JWT or `issuer`/`issuer.id` from a VC or `holder` from a VP
18
+ *
19
+ * @param input - the credential or presentation whose issuer/holder needs to be extracted.
20
+ * @param options - options for the extraction
21
+ * removeParameters - Remove all DID parameters from the issuer ID
22
+ *
23
+ * @beta This API may change without a BREAKING CHANGE notice.
24
+ */
25
+ export function extractIssuer(
26
+ input?:
27
+ | W3CVerifiableCredential
28
+ | W3CVerifiablePresentation
29
+ | CredentialPayload
30
+ | PresentationPayload
31
+ | null,
32
+ options: { removeParameters?: boolean } = {}
33
+ ): string {
34
+ if (!isDefined(input)) {
35
+ return ''
36
+ } else if (typeof input === 'string') {
37
+ // JWT
38
+ try {
39
+ const { payload } = decodeJWT(input.split(`~`)[0])
40
+ const iss = payload.iss ?? ''
41
+ return !!options.removeParameters ? removeDIDParameters(iss) : iss
42
+ } catch (e: any) {
43
+ return ''
44
+ }
45
+ } else {
46
+ // JSON
47
+ let iss: IssuerType
48
+ if (input.issuer) {
49
+ iss = input.issuer
50
+ } else if (input.holder) {
51
+ iss = input.holder
52
+ } else {
53
+ iss = ''
54
+ }
55
+ if (typeof iss !== 'string') iss = iss.id ?? ''
56
+ return !!options.removeParameters ? removeDIDParameters(iss) : iss
57
+ }
58
+ }
59
+
60
+
61
+ /**
62
+ * Remove all DID parameters from a DID url after the query part (?)
63
+ *
64
+ * @param did - the DID URL
65
+ *
66
+ * @beta This API may change without a BREAKING CHANGE notice.
67
+ */
68
+ export function removeDIDParameters(did: string): string {
69
+ return did.replace(/\?.*$/, '')
70
+ }
71
+
72
+
73
+ export function pickSigningKey(identifier: IIdentifier, keyRef?: string): IKey {
74
+ let key: IKey | undefined
75
+
76
+ if (!keyRef) {
77
+ key = identifier.keys.find((k) => k.type === 'Secp256k1' || k.type === 'Ed25519' || k.type === 'Secp256r1')
78
+ if (!key) throw Error('key_not_found: No signing key for ' + identifier.did)
79
+ } else {
80
+ key = identifier.keys.find((k) => k.kid === keyRef)
81
+ if (!key) throw Error('key_not_found: No signing key for ' + identifier.did + ' with kid ' + keyRef)
82
+ }
83
+
84
+ return key as IKey
85
+ }
86
+
87
+ export async function isRevoked(credential: VerifiableCredential, context: IAgentContext<ICredentialStatusVerifier>): Promise<boolean> {
88
+ if (!credential.credentialStatus) return false
89
+
90
+ if (typeof context.agent.checkCredentialStatus === 'function') {
91
+ const status = await context.agent.checkCredentialStatus({ credential })
92
+ return status?.revoked == true || status?.verified === false
93
+ }
94
+
95
+ throw new Error(`invalid_setup: The credential status can't be verified because there is no ICredentialStatusVerifier plugin installed.`)
96
+ }
package/src/index.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Provides a {@link @veramo/credential-w3c#CredentialPlugin | plugin} for the {@link @veramo/core#Agent} that
3
+ * implements
4
+ * {@link @veramo/core#ICredentialIssuer} interface.
5
+ *
6
+ * Provides a {@link @veramo/credential-w3c#W3cMessageHandler | plugin} for the
7
+ * {@link @veramo/message-handler#MessageHandler} that verifies Credentials and Presentations in a message.
8
+ *
9
+ * @packageDocumentation
10
+ */
11
+ export type * from './types'
12
+ export { W3cMessageHandler, MessageTypes } from './message-handler'
13
+ import { VcdmCredentialPlugin } from './action-handler'
14
+
15
+ /**
16
+ * @deprecated please use {@link VcdmCredentialPlugin} instead
17
+ * @public
18
+ */
19
+ const CredentialIssuer = VcdmCredentialPlugin
20
+ export { CredentialIssuer, VcdmCredentialPlugin }
21
+
22
+ // For backward compatibility, re-export the plugin types that were moved to core in v4
23
+ export type { ICredentialIssuer, ICredentialVerifier } from '@veramo/core'
24
+
25
+ export * from './functions'
@@ -0,0 +1,167 @@
1
+ import type {
2
+ IAgentContext,
3
+ ICredentialVerifier,
4
+ IResolver,
5
+ VerifiableCredential,
6
+ VerifiablePresentation,
7
+ } from '@veramo/core'
8
+ import { AbstractMessageHandler, Message } from '@veramo/message-handler'
9
+ import { asArray, computeEntryHash, decodeCredentialToObject, extractIssuer } from '@veramo/utils'
10
+
11
+ import {
12
+ normalizeCredential,
13
+ normalizePresentation,
14
+ validateJwtCredentialPayload,
15
+ validateJwtPresentationPayload,
16
+ }// @ts-ignore
17
+ from 'did-jwt-vc'
18
+
19
+ import { v4 as uuidv4 } from 'uuid'
20
+ import Debug from 'debug'
21
+
22
+ const debug = Debug('sphereon:vcdm:message-handler')
23
+
24
+ /**
25
+ * These types are used by `@veramo/data-store` when storing Verifiable Credentials and Presentations
26
+ *
27
+ * @internal
28
+ */
29
+ export const MessageTypes = {
30
+ /** Represents a Verifiable Credential */
31
+ vc: 'w3c.vc',
32
+ /** Represents a Verifiable Presentation */
33
+ vp: 'w3c.vp',
34
+ }
35
+
36
+ /**
37
+ * Represents the requirements that this plugin has.
38
+ * The agent that is using this plugin is expected to provide these methods.
39
+ *
40
+ * This interface can be used for static type checks, to make sure your application is properly initialized.
41
+ */
42
+ export type IContext = IAgentContext<IResolver & ICredentialVerifier>
43
+
44
+ /**
45
+ * An implementation of the {@link @veramo/message-handler#AbstractMessageHandler}.
46
+ *
47
+ * This plugin can handle incoming W3C Verifiable Credentials and Presentations and prepare them
48
+ * for internal storage as {@link @veramo/message-handler#Message} types.
49
+ *
50
+ * The current version can only handle `JWT` encoded
51
+ *
52
+ * @remarks {@link @veramo/core#IDataStore | IDataStore }
53
+ *
54
+ * @public
55
+ */
56
+ export class W3cMessageHandler extends AbstractMessageHandler {
57
+ async handle(message: Message, context: IContext): Promise<Message> {
58
+ const meta = message.getLastMetaData()
59
+
60
+ // console.log(JSON.stringify(message, null, 2))
61
+
62
+ //FIXME: messages should not be expected to be only JWT
63
+ if (meta?.type === 'JWT' && message.raw) {
64
+ const { data } = message
65
+
66
+ try {
67
+ validateJwtPresentationPayload(data)
68
+
69
+ //FIXME: flagging this for potential privacy leaks
70
+ debug('JWT is', MessageTypes.vp)
71
+ const presentation = normalizePresentation(message.raw)
72
+ const credentials = presentation.verifiableCredential
73
+
74
+ message.id = computeEntryHash(message.raw)
75
+ message.type = MessageTypes.vp
76
+ message.from = presentation.holder
77
+ message.to = presentation.verifier?.[0]
78
+
79
+ if (presentation.tag) {
80
+ message.threadId = presentation.tag
81
+ }
82
+
83
+ message.createdAt = presentation.issuanceDate
84
+ message.presentations = [presentation]
85
+ message.credentials = credentials
86
+
87
+ return message
88
+ } catch (e) {}
89
+
90
+ try {
91
+ validateJwtCredentialPayload(data)
92
+ //FIXME: flagging this for potential privacy leaks
93
+ debug('JWT is', MessageTypes.vc)
94
+ const credential = normalizeCredential(message.raw)
95
+
96
+ message.id = computeEntryHash(message.raw)
97
+ message.type = MessageTypes.vc
98
+ message.from = credential.issuer.id
99
+ message.to = credential.credentialSubject.id
100
+
101
+ if (credential.tag) {
102
+ message.threadId = credential.tag
103
+ }
104
+
105
+ message.createdAt = credential.issuanceDate
106
+ message.credentials = [credential]
107
+ return message
108
+ } catch (e) {}
109
+ }
110
+
111
+ // LDS Verification and Handling
112
+ if (message.type === MessageTypes.vc && message.data) {
113
+ // verify credential
114
+ const credential = message.data as VerifiableCredential
115
+
116
+ const result = await context.agent.verifyCredential({ credential })
117
+ if (result.verified) {
118
+ message.id = computeEntryHash(message.raw || message.id || uuidv4())
119
+ message.type = MessageTypes.vc
120
+ message.from = extractIssuer(credential)
121
+ message.to = credential.credentialSubject.id
122
+
123
+ if (credential.tag) {
124
+ message.threadId = credential.tag
125
+ }
126
+
127
+ message.createdAt = credential.issuanceDate
128
+ message.credentials = [credential]
129
+ return message
130
+ } else {
131
+ throw new Error(result.error?.message)
132
+ }
133
+ }
134
+
135
+ if (message.type === MessageTypes.vp && message.data) {
136
+ // verify presentation
137
+ const presentation = message.data as VerifiablePresentation
138
+
139
+ // throws on error.
140
+ const result = await context.agent.verifyPresentation({
141
+ presentation,
142
+ // FIXME: HARDCODED CHALLENGE VERIFICATION FOR NOW
143
+ challenge: 'VERAMO',
144
+ domain: 'VERAMO',
145
+ })
146
+ if (result.verified) {
147
+ message.id = computeEntryHash(message.raw || message.id || uuidv4())
148
+ message.type = MessageTypes.vp
149
+ message.from = presentation.holder
150
+ // message.to = presentation.verifier?.[0]
151
+
152
+ if (presentation.tag) {
153
+ message.threadId = presentation.tag
154
+ }
155
+
156
+ // message.createdAt = presentation.issuanceDate
157
+ message.presentations = [presentation]
158
+ message.credentials = asArray(presentation.verifiableCredential).map(decodeCredentialToObject)
159
+ return message
160
+ } else {
161
+ throw new Error(result.error?.message)
162
+ }
163
+ }
164
+
165
+ return super.handle(message, context)
166
+ }
167
+ }