Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added policies to verifyPresentation in credential-w3c module #990

Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion packages/cli/src/presentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ presentation
challenge: options.challenge,
domain: options.domain,
})
if (result === true) {
if (result.verified === true) {
console.log('Presentation was verified successfully.')
} else {
console.error('Presentation could not be verified.')
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export * from './types/IKeyManager'
export * from './types/IMessage'
export * from './types/IMessageHandler'
export * from './types/IResolver'
export * from './types/IError'
export * from './types/vc-data-model'

/**
Expand Down
17 changes: 17 additions & 0 deletions packages/core/src/types/IError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Error interface
* @public
*/

export interface IError {
mirceanis marked this conversation as resolved.
Show resolved Hide resolved

/**
* The details of the error being throw or forwarded
*/
errorMessage?: string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's use message to be compatible with native Error objects

Suggested change
errorMessage?: string
message: string


/**
* The code for the error being throw
*/
errorCode?: string
}
60 changes: 59 additions & 1 deletion packages/credential-w3c/plugin.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,10 @@
"type": "boolean",
"description": "When dealing with JSON-LD you also MUST provide the proper contexts. Set this to `true` ONLY if you want the '@context' URLs to be fetched in case they are not pre-loaded. The context definitions SHOULD rather be provided at startup instead of being fetched.",
"default": false
},
"policies": {
"$ref": "#/components/schemas/VerifyPresentationPolicies",
"description": "Verification Policies for the verifiable presentation These will also be forwarded to the lower level module"
}
},
"required": [
Expand All @@ -419,6 +423,60 @@
}
],
"description": "Represents a signed Verifiable Presentation (includes proof) in either JSON or compact JWT format. See {@link https://www.w3.org/TR/vc-data-model/#credentials | VC data model }"
},
"VerifyPresentationPolicies": {
"type": "object",
"properties": {
"now": {
"type": "number"
},
"issuanceDate": {
"type": "boolean"
},
"issuedAtDate": {
"type": "boolean"
},
"expirationDate": {
"type": "boolean"
}
},
"additionalProperties": {
"description": "Other options can be specified for verification. They will be forwarded to the lower level modules that perform the checks"
}
},
"IVerifyPresentationResponse": {
"type": "object",
"properties": {
"verified": {
"type": "boolean",
"description": "This value is used to transmit the result of verification."
},
"error": {
"$ref": "#/components/schemas/IError",
"description": "Optional Error object for the but currently the machine readable errors are not expored from DID-JWT package to be imported here"
}
},
"required": [
"verified"
],
"additionalProperties": {
"description": "Other options can be specified for verification. They will be forwarded to the lower level modules. that performt the checks"
},
"description": "Encapsulates the response object to verifyPresentation method after verifying a\n {@link https://www.w3.org/TR/vc-data-model/#presentations | W3C Verifiable Presentation }"
},
"IError": {
"type": "object",
"properties": {
"errorMessage": {
"type": "string",
"description": "The details of the error being throw or forwarded"
},
"errorCode": {
"type": "string",
"description": "The code for the error being throw"
}
},
"description": "Error interface"
}
},
"methods": {
Expand Down Expand Up @@ -455,7 +513,7 @@
"$ref": "#/components/schemas/IVerifyPresentationArgs"
},
"returnType": {
"type": "boolean"
"$ref": "#/components/schemas/IVerifyPresentationResponse"
}
}
}
Expand Down
157 changes: 157 additions & 0 deletions packages/credential-w3c/src/__tests__/issue-verify-flow.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import {
mirceanis marked this conversation as resolved.
Show resolved Hide resolved
createAgent,
CredentialPayload,
IDIDManager,
IIdentifier,
IKeyManager,
IResolver,
TAgent,
} from '../../../core/src'
import { CredentialIssuer, ICredentialIssuer } from '../../../credential-w3c/src'
import { DIDManager, MemoryDIDStore } from '../../../did-manager/src'
import { KeyManager, MemoryKeyStore, MemoryPrivateKeyStore } from '../../../key-manager/src'
import { KeyManagementSystem } from '../../../kms-local/src'
import { getDidKeyResolver, KeyDIDProvider } from '../../../did-provider-key/src'
import { DIDResolverPlugin } from '../../../did-resolver/src'
import { EthrDIDProvider } from "../../../did-provider-ethr/src";
import { ContextDoc } from '../../../credential-ld/src/types'
import { Resolver } from 'did-resolver'
import { getResolver as ethrDidResolver } from 'ethr-did-resolver'

jest.setTimeout(300000)

const customContext: Record<string, ContextDoc> = {
'custom:example.context': {
'@context': {
nothing: 'custom:example.context#blank',
},
},
}

const infuraProjectId = '3586660d179141e3801c3895de1c2eba'

describe('credential-w3c full flow', () => {
let didKeyIdentifier: IIdentifier
let didEthrIdentifier: IIdentifier
let agent: TAgent<IResolver & IKeyManager & IDIDManager & ICredentialIssuer>

beforeAll(async () => {
agent = createAgent({
plugins: [
new KeyManager({
store: new MemoryKeyStore(),
kms: {
local: new KeyManagementSystem(new MemoryPrivateKeyStore()),
},
}),
new DIDManager({
providers: {
'did:key': new KeyDIDProvider({ defaultKms: 'local' }),
'did:ethr:goerli': new EthrDIDProvider({
defaultKms: 'local',
network: 'goerli',
}),
},
store: new MemoryDIDStore(),
defaultProvider: 'did:key',
}),
new DIDResolverPlugin({
resolver: new Resolver({
...getDidKeyResolver(),
...ethrDidResolver({ infuraProjectId, }),
}),
}),
new CredentialIssuer(),
],
})
didKeyIdentifier = await agent.didManagerCreate()
didEthrIdentifier = await agent.didManagerCreate({ provider: "did:ethr:goerli" })
})

it('verify a verifiablePresentation', async () => {
const credential: CredentialPayload = {
issuer: didKeyIdentifier.did,
'@context': ['custom:example.context'],
credentialSubject: {
nothing: 'else matters',
},
}
const verifiableCredential1 = await agent.createVerifiableCredential({
credential,
proofFormat: 'jwt',
})

const verifiablePresentation = await agent.createVerifiablePresentation({
presentation: {
verifiableCredential: [verifiableCredential1],
holder: didKeyIdentifier.did
},
challenge: "VERAMO",
proofFormat: 'jwt',
})

expect(verifiablePresentation).toBeDefined()

const response = await agent.verifyPresentation({
presentation: verifiablePresentation,
challenge: "VERAMO",
})

expect(response.verified).toBe(true)
})

it('fails the verification of an expired credential', async () => {
const presentationJWT = 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NjAyOTcyMTAsInZwIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZVByZXNlbnRhdGlvbiJdLCJ2ZXJpZmlhYmxlQ3JlZGVudGlhbCI6WyJleUpoYkdjaU9pSkZaRVJUUVNJc0luUjVjQ0k2SWtwWFZDSjkuZXlKbGVIQWlPakUyTmpBeU9UY3lNVEFzSW5aaklqcDdJa0JqYjI1MFpYaDBJanBiSW1oMGRIQnpPaTh2ZDNkM0xuY3pMbTl5Wnk4eU1ERTRMMk55WldSbGJuUnBZV3h6TDNZeElpd2lZM1Z6ZEc5dE9tVjRZVzF3YkdVdVkyOXVkR1Y0ZENKZExDSjBlWEJsSWpwYklsWmxjbWxtYVdGaWJHVkRjbVZrWlc1MGFXRnNJbDBzSW1OeVpXUmxiblJwWVd4VGRXSnFaV04wSWpwN0ltNXZkR2hwYm1jaU9pSmxiSE5sSUcxaGRIUmxjbk1pZlgwc0ltNWlaaUk2TVRZMk1ESTVOekl4TUN3aWFYTnpJam9pWkdsa09tdGxlVHA2TmsxcmFWVTNVbk5hVnpOeWFXVmxRMjg1U25OMVVEUnpRWEZYZFdGRE0zbGhjbWwxWVZCMlVXcHRZVzVsWTFBaWZRLkZhdzBEUWNNdXpacEVkcy1LR3dOalMyM2IzbUEzZFhQWXBQcGJzNmRVSnhIOVBrZzVieGF3UDVwMlNPajdQM25IdEpCR3lwTjJ3NzRfZjc3SjF5dUJ3Il19LCJuYmYiOjE2NjAyOTcyMTAsImlzcyI6ImRpZDprZXk6ejZNa2lVN1JzWlczcmllZUNvOUpzdVA0c0FxV3VhQzN5YXJpdWFQdlFqbWFuZWNQIn0.YcYbyqVlD8YsTjVw0kCEs0P_ie6SFMakf_ncPntEjsmS9C4cKyiS50ZhNkOv0R3Roy1NrzX7h93WBU55KeJlCw'

const response = await agent.verifyPresentation({
presentation: presentationJWT,
})

expect(response.verified).toBe(false)
expect(response.error).toBeDefined()
expect(response.error?.errorMessage).toContain('JWT has expired')
})


it('fails the verification with nbf in the future',async () => {
const presentationJWT = 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJ2cCI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVQcmVzZW50YXRpb24iXSwidmVyaWZpYWJsZUNyZWRlbnRpYWwiOlsiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNklrcFhWQ0o5LmV5SjJZeUk2ZXlKQVkyOXVkR1Y0ZENJNld5Sm9kSFJ3Y3pvdkwzZDNkeTUzTXk1dmNtY3ZNakF4T0M5amNtVmtaVzUwYVdGc2N5OTJNU0lzSW1OMWMzUnZiVHBsZUdGdGNHeGxMbU52Ym5SbGVIUWlYU3dpZEhsd1pTSTZXeUpXWlhKcFptbGhZbXhsUTNKbFpHVnVkR2xoYkNKZExDSmpjbVZrWlc1MGFXRnNVM1ZpYW1WamRDSTZleUp1YjNSb2FXNW5Jam9pWld4elpTQnRZWFIwWlhKekluMTlMQ0p1WW1ZaU9qRXhOall3TWprNE5UZzRMQ0pwYzNNaU9pSmthV1E2YTJWNU9ubzJUV3QyYlhCeFRXbDFOM2h1U25kVE9YQkVSR0ZSYW1oQ1dUWndlbU00V1RKQ2FWRnhSWFUwZW1GRldFMVdUQ0o5LnA4Y2FTS1pTcGdISm1TRzhMekpnSWlWMzFRU3NjOEJ2anZuQ1JrOEM3X1UxLXV5cS11MHlQcDdjRWlSOUtXTnprN2RDQlBiR2pBRGRiNC0tV3V5LUNRIl19LCJuYmYiOjI2NjAyOTg1ODgsImlzcyI6ImRpZDprZXk6ejZNa3ZtcHFNaXU3eG5Kd1M5cEREYVFqaEJZNnB6YzhZMkJpUXFFdTR6YUVYTVZMIiwibm9uY2UiOiJWRVJBTU8ifQ.F-uiI2iVMcdm1VFzkXgtZqq8QGw5XnyEI36vGblBluHnklnNYNmE5eluQ23dbcduGWSe3ZJJ65C7HrPTUoXvDA'

const response = await agent.verifyPresentation({
presentation: presentationJWT,
})

expect(response.verified).toBe(false)
expect(response.error).toBeDefined()
expect(response.error?.errorMessage).toContain('JWT not valid before nbf')
})

/**
* These tests can be uncommented out when the did-jwt starts to support the policies merge request
*/

// it('passes the verification of an expired credential with policy exp false',async () => {
// const presentationJWT = 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJ2cCI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVQcmVzZW50YXRpb24iXSwidmVyaWZpYWJsZUNyZWRlbnRpYWwiOlsiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNklrcFhWQ0o5LmV5SjJZeUk2ZXlKQVkyOXVkR1Y0ZENJNld5Sm9kSFJ3Y3pvdkwzZDNkeTUzTXk1dmNtY3ZNakF4T0M5amNtVmtaVzUwYVdGc2N5OTJNU0lzSW1OMWMzUnZiVHBsZUdGdGNHeGxMbU52Ym5SbGVIUWlYU3dpZEhsd1pTSTZXeUpXWlhKcFptbGhZbXhsUTNKbFpHVnVkR2xoYkNKZExDSmpjbVZrWlc1MGFXRnNVM1ZpYW1WamRDSTZleUp1YjNSb2FXNW5Jam9pWld4elpTQnRZWFIwWlhKekluMTlMQ0p1WW1ZaU9qRXhOall3TWprNE5UZzRMQ0pwYzNNaU9pSmthV1E2YTJWNU9ubzJUV3QyYlhCeFRXbDFOM2h1U25kVE9YQkVSR0ZSYW1oQ1dUWndlbU00V1RKQ2FWRnhSWFUwZW1GRldFMVdUQ0o5LnA4Y2FTS1pTcGdISm1TRzhMekpnSWlWMzFRU3NjOEJ2anZuQ1JrOEM3X1UxLXV5cS11MHlQcDdjRWlSOUtXTnprN2RDQlBiR2pBRGRiNC0tV3V5LUNRIl19LCJuYmYiOjI2NjAyOTg1ODgsImlzcyI6ImRpZDprZXk6ejZNa3ZtcHFNaXU3eG5Kd1M5cEREYVFqaEJZNnB6YzhZMkJpUXFFdTR6YUVYTVZMIiwibm9uY2UiOiJWRVJBTU8ifQ.F-uiI2iVMcdm1VFzkXgtZqq8QGw5XnyEI36vGblBluHnklnNYNmE5eluQ23dbcduGWSe3ZJJ65C7HrPTUoXvDA'

// const response = await agent.verifyPresentation({
// presentation: presentationJWT,
// policies: {
// exp: false
// }
// })

// expect(response.verified).toBe(true)
// })

// it('passes the verification with nbf in the future with policy nbf false',async () => {
// const presentationJWT = 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJ2cCI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVQcmVzZW50YXRpb24iXSwidmVyaWZpYWJsZUNyZWRlbnRpYWwiOlsiZXlKaGJHY2lPaUpGWkVSVFFTSXNJblI1Y0NJNklrcFhWQ0o5LmV5SjJZeUk2ZXlKQVkyOXVkR1Y0ZENJNld5Sm9kSFJ3Y3pvdkwzZDNkeTUzTXk1dmNtY3ZNakF4T0M5amNtVmtaVzUwYVdGc2N5OTJNU0lzSW1OMWMzUnZiVHBsZUdGdGNHeGxMbU52Ym5SbGVIUWlYU3dpZEhsd1pTSTZXeUpXWlhKcFptbGhZbXhsUTNKbFpHVnVkR2xoYkNKZExDSmpjbVZrWlc1MGFXRnNVM1ZpYW1WamRDSTZleUp1YjNSb2FXNW5Jam9pWld4elpTQnRZWFIwWlhKekluMTlMQ0p1WW1ZaU9qRXhOall3TWprNE5UZzRMQ0pwYzNNaU9pSmthV1E2YTJWNU9ubzJUV3QyYlhCeFRXbDFOM2h1U25kVE9YQkVSR0ZSYW1oQ1dUWndlbU00V1RKQ2FWRnhSWFUwZW1GRldFMVdUQ0o5LnA4Y2FTS1pTcGdISm1TRzhMekpnSWlWMzFRU3NjOEJ2anZuQ1JrOEM3X1UxLXV5cS11MHlQcDdjRWlSOUtXTnprN2RDQlBiR2pBRGRiNC0tV3V5LUNRIl19LCJuYmYiOjI2NjAyOTg1ODgsImlzcyI6ImRpZDprZXk6ejZNa3ZtcHFNaXU3eG5Kd1M5cEREYVFqaEJZNnB6YzhZMkJpUXFFdTR6YUVYTVZMIiwibm9uY2UiOiJWRVJBTU8ifQ.F-uiI2iVMcdm1VFzkXgtZqq8QGw5XnyEI36vGblBluHnklnNYNmE5eluQ23dbcduGWSe3ZJJ65C7HrPTUoXvDA'

// const response = await agent.verifyPresentation({
// presentation: presentationJWT,
// policies: {
// nbf: false
// }
// })

// expect(response.verified).toBe(true)
// })
})
62 changes: 57 additions & 5 deletions packages/credential-w3c/src/action-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
IKeyManager,
IPluginMethodMap,
IResolver,
IError,
PresentationPayload,
VerifiableCredential,
VerifiablePresentation,
Expand Down Expand Up @@ -215,6 +216,51 @@ export interface IVerifyPresentationArgs {
*/
fetchRemoteContexts?: boolean


/**
* Verification Policies for the verifiable presentation
* These will also be forwarded to the lower level module
*/
policies?: VerifyPresentationPolicies

/**
* Other options can be specified for verification.
* They will be forwarded to the lower level modules. that performt the checks
*/
[x: string]: any
}

export interface VerifyPresentationPolicies {
mirceanis marked this conversation as resolved.
Show resolved Hide resolved
now?: number
issuanceDate?: boolean
issuedAtDate?: boolean
expirationDate?: boolean

/**
* Other options can be specified for verification.
* They will be forwarded to the lower level modules that perform the checks
*/
[x: string]: any
}

/**
* Encapsulates the response object to verifyPresentation method after verifying a
* {@link https://www.w3.org/TR/vc-data-model/#presentations | W3C Verifiable Presentation}
*
* @public
*/
export interface IVerifyPresentationResponse {
mirceanis marked this conversation as resolved.
Show resolved Hide resolved
/**
* This value is used to transmit the result of verification.
*/
verified: boolean

/**
* Optional Error object for the
* but currently the machine readable errors are not expored from DID-JWT package to be imported here
*/
error?: IError

/**
* Other options can be specified for verification.
* They will be forwarded to the lower level modules. that performt the checks
Expand Down Expand Up @@ -286,7 +332,7 @@ export interface ICredentialIssuer extends IPluginMethodMap {
*
* @remarks Please see {@link https://www.w3.org/TR/vc-data-model/#presentations | Verifiable Credential data model}
*/
verifyPresentation(args: IVerifyPresentationArgs, context: IContext): Promise<boolean>
verifyPresentation(args: IVerifyPresentationArgs, context: IContext): Promise<IVerifyPresentationResponse>
}

/**
Expand Down Expand Up @@ -542,7 +588,7 @@ export class CredentialIssuer implements IAgentPlugin {
}

/** {@inheritdoc ICredentialIssuer.verifyPresentation} */
async verifyPresentation(args: IVerifyPresentationArgs, context: IContext): Promise<boolean> {
async verifyPresentation(args: IVerifyPresentationArgs, context: IContext): Promise<IVerifyPresentationResponse> {
const presentation = args.presentation
if (typeof presentation === 'string' || (<VerifiablePresentation>presentation)?.proof?.jwt) {
// JWT
Expand Down Expand Up @@ -573,11 +619,17 @@ export class CredentialIssuer implements IAgentPlugin {
challenge: args.challenge,
domain: args.domain,
audience,
policies: args.policies
mirceanis marked this conversation as resolved.
Show resolved Hide resolved
})
return true
return { verified: true }
} catch (e: any) {
//TODO: return a more detailed reason for failure
return false
const errorCodes = ['invalid_jwt','invalid_config', 'invalid_signature', 'not_supported', 'no_suitable_keys', 'resolver_error']
if (errorCodes.some(errorCode => e.message.includes(errorCode))) {
return { verified: false, error: { errorMessage: e.message } }
}
else {
throw e
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like this separation of concerns between legitimate errors and exceptional situations.

However, I'm not yet 100% sure we should be introducing an extra branch, though.
I'm concerned about the usability here where developers have to handle 3 situations: valid response, invalid response and thrown error.
This is not a critique of your approach (which I've also described in the past), but rather me venting my uncertainty :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understandable, how do you propose we approach this issue?

Copy link
Member

@mirceanis mirceanis Aug 16, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose the simple answer would be to not re-throw an error in the exceptional case, but to return an object just like for the legitimate failures.
The difference would be that for the legitimate failures we can also identify the cause and populate the errorCode property of the error.

What do you think?

}
}
} else {
// JSON-LD
Expand Down
Loading