From 72450318c21fda718bc82be94e33e82be677beb5 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 9 Oct 2023 21:16:43 +0200 Subject: [PATCH 001/115] fix: port openid4vci from paradym wallet Signed-off-by: Martin Auer --- packages/core/src/index.ts | 10 +- packages/core/src/modules/vc/index.ts | 2 +- packages/openid4vc-client/jest.config.ts | 1 + packages/openid4vc-client/package.json | 15 +- .../src/OpenId4VcClientApi.ts | 2 +- .../src/OpenId4VcClientModule.ts | 3 + .../src/OpenId4VcClientService.ts | 460 ++++++++++++---- .../src/OpenId4VcClientServiceOptions.ts | 29 +- packages/openid4vc-client/src/index.ts | 18 +- .../presentations/OpenId4VpClientService.ts | 213 ++++++++ .../PresentationExchangeService.ts | 247 +++++++++ .../src/presentations/example.md | 66 +++ .../src/presentations/fixtures.ts | 76 +++ .../src/presentations/index.ts | 6 + .../selection/PexCredentialSelection.ts | 303 +++++++++++ .../src/presentations/selection/index.ts | 2 + .../src/presentations/selection/types.ts | 108 ++++ .../src/presentations/transform.ts | 65 +++ .../openid4vc-client/src/utils/Formats.ts | 41 ++ .../src/utils/IssuerMetadataUtils.ts | 113 ++++ .../__tests__/claimFormatMapping.test.ts | 45 ++ .../src/utils/claimFormatMapping.ts | 40 ++ packages/openid4vc-client/src/utils/index.ts | 2 + .../openid4vc-client/src/utils/metadata.ts | 70 +++ .../OpenId4VcClientModule.test.ts | 12 +- packages/openid4vc-client/tests/fixtures.ts | 140 ++++- .../tests/openid4vc-client.e2e.test.ts | 325 +++++++++--- yarn.lock | 494 +++++++++++++++++- 28 files changed, 2687 insertions(+), 221 deletions(-) create mode 100644 packages/openid4vc-client/src/presentations/OpenId4VpClientService.ts create mode 100644 packages/openid4vc-client/src/presentations/PresentationExchangeService.ts create mode 100644 packages/openid4vc-client/src/presentations/example.md create mode 100644 packages/openid4vc-client/src/presentations/fixtures.ts create mode 100644 packages/openid4vc-client/src/presentations/index.ts create mode 100644 packages/openid4vc-client/src/presentations/selection/PexCredentialSelection.ts create mode 100644 packages/openid4vc-client/src/presentations/selection/index.ts create mode 100644 packages/openid4vc-client/src/presentations/selection/types.ts create mode 100644 packages/openid4vc-client/src/presentations/transform.ts create mode 100644 packages/openid4vc-client/src/utils/Formats.ts create mode 100644 packages/openid4vc-client/src/utils/IssuerMetadataUtils.ts create mode 100644 packages/openid4vc-client/src/utils/__tests__/claimFormatMapping.test.ts create mode 100644 packages/openid4vc-client/src/utils/claimFormatMapping.ts create mode 100644 packages/openid4vc-client/src/utils/index.ts create mode 100644 packages/openid4vc-client/src/utils/metadata.ts rename packages/openid4vc-client/{src/__tests__ => tests}/OpenId4VcClientModule.test.ts (61%) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 4d99d06980..99406b3d2e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -61,7 +61,15 @@ export * from './modules/oob' export * from './modules/dids' export * from './modules/vc' export * from './modules/cache' -export { JsonEncoder, JsonTransformer, isJsonObject, isValidJweStructure, TypedArrayEncoder, Buffer } from './utils' +export { + JsonEncoder, + JsonTransformer, + isJsonObject, + isValidJweStructure, + TypedArrayEncoder, + Buffer, + asArray, +} from './utils' export * from './logger' export * from './error' export * from './wallet/error' diff --git a/packages/core/src/modules/vc/index.ts b/packages/core/src/modules/vc/index.ts index 47571cdecb..84a0da17c7 100644 --- a/packages/core/src/modules/vc/index.ts +++ b/packages/core/src/modules/vc/index.ts @@ -1,6 +1,6 @@ export * from './W3cCredentialService' export * from './W3cCredentialServiceOptions' -export * from './repository/W3cCredentialRecord' +export * from './repository' export * from './W3cCredentialsModule' export * from './W3cCredentialsApi' export * from './models' diff --git a/packages/openid4vc-client/jest.config.ts b/packages/openid4vc-client/jest.config.ts index 93c0197296..8641cf4d67 100644 --- a/packages/openid4vc-client/jest.config.ts +++ b/packages/openid4vc-client/jest.config.ts @@ -6,6 +6,7 @@ import packageJson from './package.json' const config: Config.InitialOptions = { ...base, + displayName: packageJson.name, setupFilesAfterEnv: ['./tests/setup.ts'], } diff --git a/packages/openid4vc-client/package.json b/packages/openid4vc-client/package.json index a0bcdf214f..9c50ae7100 100644 --- a/packages/openid4vc-client/package.json +++ b/packages/openid4vc-client/package.json @@ -25,11 +25,22 @@ }, "dependencies": { "@aries-framework/core": "0.4.2", - "@sphereon/openid4vci-client": "^0.4.0", - "@stablelib/random": "^1.0.2" + "@sphereon/did-auth-siop": "^0.4.2", + "@sphereon/oid4vci-client": "^0.7.3", + "@sphereon/oid4vci-common": "^0.7.3", + "@sphereon/pex": "^2.1.3-unstable.6", + "@sphereon/pex-models": "^2.1.1", + "@sphereon/ssi-types": "^0.17.5", + "@stablelib/random": "^1.0.2", + "fast-text-encoding": "^1.0.6", + "jsonpath": "^1.1.1", + "sha.js": "^2.4.11" }, "devDependencies": { + "@aries-framework/askar": "0.4.2", "@aries-framework/node": "0.4.2", + "@hyperledger/aries-askar-nodejs": "^0.1.0", + "@types/jsonpath": "^0.2.0", "nock": "^13.3.0", "rimraf": "^4.4.0", "typescript": "~4.9.5" diff --git a/packages/openid4vc-client/src/OpenId4VcClientApi.ts b/packages/openid4vc-client/src/OpenId4VcClientApi.ts index a423a0c174..5049c518ba 100644 --- a/packages/openid4vc-client/src/OpenId4VcClientApi.ts +++ b/packages/openid4vc-client/src/OpenId4VcClientApi.ts @@ -5,7 +5,7 @@ import type { } from './OpenId4VcClientServiceOptions' import type { W3cCredentialRecord } from '@aries-framework/core' -import { AgentContext, injectable } from '@aries-framework/core' +import { injectable, AgentContext } from '@aries-framework/core' import { OpenId4VcClientService } from './OpenId4VcClientService' import { AuthFlowType } from './OpenId4VcClientServiceOptions' diff --git a/packages/openid4vc-client/src/OpenId4VcClientModule.ts b/packages/openid4vc-client/src/OpenId4VcClientModule.ts index ad6381da52..6eb598b3d3 100644 --- a/packages/openid4vc-client/src/OpenId4VcClientModule.ts +++ b/packages/openid4vc-client/src/OpenId4VcClientModule.ts @@ -4,6 +4,7 @@ import { AgentConfig } from '@aries-framework/core' import { OpenId4VcClientApi } from './OpenId4VcClientApi' import { OpenId4VcClientService } from './OpenId4VcClientService' +import { OpenId4VpClientService, PresentationExchangeService } from './presentations' /** * @public @@ -27,5 +28,7 @@ export class OpenId4VcClientModule implements Module { // Services dependencyManager.registerSingleton(OpenId4VcClientService) + dependencyManager.registerSingleton(OpenId4VpClientService) + dependencyManager.registerSingleton(PresentationExchangeService) } } diff --git a/packages/openid4vc-client/src/OpenId4VcClientService.ts b/packages/openid4vc-client/src/OpenId4VcClientService.ts index d4ad452c2b..21f59d0d0b 100644 --- a/packages/openid4vc-client/src/OpenId4VcClientService.ts +++ b/packages/openid4vc-client/src/OpenId4VcClientService.ts @@ -5,17 +5,26 @@ import type { SupportedCredentialFormats, ProofOfPossessionRequirements, } from './OpenId4VcClientServiceOptions' +import type { OpenIdCredentialFormatProfile } from './utils' import type { AgentContext, W3cVerifiableCredential, VerificationMethod, JwaSignatureAlgorithm, - W3cCredentialRecord, W3cVerifyCredentialResult, } from '@aries-framework/core' -import type { CredentialMetadata, CredentialResponse, Jwt, OpenIDResponse } from '@sphereon/openid4vci-client' +import type { + CredentialOfferFormat, + CredentialOfferPayloadV1_0_08, + CredentialOfferRequestWithBaseUrl, + CredentialResponse, + CredentialSupported, + Jwt, + OpenIDResponse, +} from '@sphereon/oid4vci-common' import { + W3cCredentialRecord, ClaimFormat, getJwkClassFromJwaSignatureAlgorithm, W3cJwtVerifiableCredential, @@ -27,27 +36,40 @@ import { InjectionSymbols, JsonEncoder, JsonTransformer, - JwsService, - Logger, TypedArrayEncoder, - W3cCredentialService, W3cJsonLdVerifiableCredential, getJwkFromKey, getSupportedVerificationMethodTypesFromKeyType, getJwkClassFromKeyType, parseDid, SignatureSuiteRegistry, + JwsService, + Logger, + W3cCredentialService, + W3cCredentialRepository, } from '@aries-framework/core' -import { - AuthzFlowType, - CodeChallengeMethod, - CredentialRequestClientBuilder, - OpenID4VCIClient, - ProofOfPossessionBuilder, -} from '@sphereon/openid4vci-client' +import { CredentialRequestClientBuilder, OpenID4VCIClient, ProofOfPossessionBuilder } from '@sphereon/oid4vci-client' +import { AuthzFlowType, CodeChallengeMethod, OpenId4VCIVersion } from '@sphereon/oid4vci-common' import { randomStringForEntropy } from '@stablelib/random' import { supportedCredentialFormats, AuthFlowType } from './OpenId4VcClientServiceOptions' +import { setOpenId4VcCredentialMetadata, fromOpenIdCredentialFormatProfileToDifClaimFormat } from './utils' +import { getUniformFormat } from './utils/Formats' +import { getSupportedCredentials } from './utils/IssuerMetadataUtils' + +/** + * The type of a credential offer entry. For each item in `credentials` array, the type MUST be one of the following: + * - CredentialSupported, when the value is a string and points to a credential from the `credentials_supported` array. + * - InlineCredentialOffer, when the value is a JSON object that represents an inline credential offer. + */ +export enum OfferedCredentialType { + CredentialSupported = 'CredentialSupported', + InlineCredentialOffer = 'InlineCredentialOffer', +} + +export type OfferedCredentialsWithMetadata = + | { credentialSupported: CredentialSupported; type: OfferedCredentialType.CredentialSupported } + | { inlineCredentialOffer: CredentialOfferFormat; type: OfferedCredentialType.InlineCredentialOffer } const flowTypeMapping = { [AuthFlowType.AuthorizationCodeFlow]: AuthzFlowType.AUTHORIZATION_CODE_FLOW, @@ -61,14 +83,17 @@ const flowTypeMapping = { export class OpenId4VcClientService { private logger: Logger private w3cCredentialService: W3cCredentialService + private w3cCredentialRepository: W3cCredentialRepository private jwsService: JwsService public constructor( @inject(InjectionSymbols.Logger) logger: Logger, w3cCredentialService: W3cCredentialService, + w3cCredentialRepository: W3cCredentialRepository, jwsService: JwsService ) { this.w3cCredentialService = w3cCredentialService + this.w3cCredentialRepository = w3cCredentialRepository this.jwsService = jwsService this.logger = logger } @@ -86,8 +111,8 @@ export class OpenId4VcClientService { ) } - const client = await OpenID4VCIClient.initiateFromURI({ - issuanceInitiationURI: options.initiationUri, + const client = await OpenID4VCIClient.fromURI({ + uri: options.initiationUri, flowType: AuthzFlowType.AUTHORIZATION_CODE_FLOW, }) const codeVerifier = this.generateCodeVerifier() @@ -130,12 +155,12 @@ export class OpenId4VcClientService { const flowType = flowTypeMapping[options.flowType] if (!flowType) { throw new AriesFrameworkError( - `Unsupported flowType ${options.flowType}. Valid values are ${Object.values(AuthFlowType)}` + `Unsupported flowType ${options.flowType}. Valid values are ${Object.values(AuthFlowType).join(', ')}` ) } - const client = await OpenID4VCIClient.initiateFromURI({ - issuanceInitiationURI: options.issuerUri, + const client = await OpenID4VCIClient.fromURI({ + uri: options.issuerUri, flowType, }) @@ -162,25 +187,40 @@ export class OpenId4VcClientService { tokenEndpoint: serverMetadata.token_endpoint, }) - const credentialsSupported = client.getCredentialsSupported(true) - this.logger.debug('Full server metadata', serverMetadata) // Loop through all the credentialTypes in the credential offer - for (const credentialType of client.getCredentialTypesFromInitiation()) { - const credentialMetadata = credentialsSupported[credentialType] + for (const offeredCredential of this.getOfferedCredentialsWithMetadata(client)) { + const format = ( + isInlineCredentialOffer(offeredCredential) + ? offeredCredential.inlineCredentialOffer.format + : offeredCredential.credentialSupported.format + ) as SupportedCredentialFormats + + // TODO: support inline credential offers. Not clear to me how to determine the did method / alg, etc.. + if (offeredCredential.type === OfferedCredentialType.InlineCredentialOffer) { + // Check if the format is supported/allowed + if (!allowedCredentialFormats.includes(format)) continue + } else { + const supportedCredentialMetadata = offeredCredential.credentialSupported + + // FIXME + // If the credential id ends with the format, it is a v8 credential supported that has been + // split into multiple entries (each entry can now only have one format). For now we continue + // as assume there will be another entry with the correct format. + if (supportedCredentialMetadata.id?.endsWith(`-${supportedCredentialMetadata.format}`)) { + const uniformFormat = getUniformFormat(supportedCredentialMetadata.format) as SupportedCredentialFormats + if (!allowedCredentialFormats.includes(uniformFormat)) continue + } + } // Get all options for the credential request (such as which kid to use, the signature algorithm, etc) - const { verificationMethod, credentialFormat, signatureAlgorithm } = await this.getCredentialRequestOptions( - agentContext, - { - allowedCredentialFormats, - allowedProofOfPossessionSignatureAlgorithms, - credentialMetadata, - credentialType, - proofOfPossessionVerificationMethodResolver: options.proofOfPossessionVerificationMethodResolver, - } - ) + const { verificationMethod, signatureAlgorithm } = await this.getCredentialRequestOptions(agentContext, { + allowedCredentialFormats, + allowedProofOfPossessionSignatureAlgorithms, + offeredCredentialWithMetadata: offeredCredential, + proofOfPossessionVerificationMethodResolver: options.proofOfPossessionVerificationMethodResolver, + }) // Create the proof of possession const proofInput = await ProofOfPossessionBuilder.fromAccessTokenResponse({ @@ -188,33 +228,67 @@ export class OpenId4VcClientService { callbacks: { signCallback: this.signCallback(agentContext, verificationMethod), }, + version: client.version(), }) .withEndpointMetadata(serverMetadata) .withAlg(signatureAlgorithm) + .withClientId(verificationMethod.controller) .withKid(verificationMethod.id) .build() this.logger.debug('Generated JWS', proofInput) // Acquire the credential - const credentialRequestClient = CredentialRequestClientBuilder.fromIssuanceInitiationURI({ - uri: options.issuerUri, - metadata: serverMetadata, - }) + const credentialRequestClient = ( + await CredentialRequestClientBuilder.fromURI({ + uri: options.issuerUri, + metadata: serverMetadata, + }) + ) .withTokenFromResponse(accessToken) .build() - const credentialResponse = await credentialRequestClient.acquireCredentialsUsingProof({ - proofInput, - credentialType, - format: credentialFormat, - }) + let credentialResponse: OpenIDResponse + + if (isInlineCredentialOffer(offeredCredential)) { + credentialResponse = await credentialRequestClient.acquireCredentialsUsingProof({ + proofInput, + credentialTypes: offeredCredential.inlineCredentialOffer.types, + format: offeredCredential.inlineCredentialOffer.format, + }) + } else { + credentialResponse = await credentialRequestClient.acquireCredentialsUsingProof({ + proofInput, + credentialTypes: offeredCredential.type, + format: offeredCredential.credentialSupported.format, + }) + } - const storedCredential = await this.handleCredentialResponse(agentContext, credentialResponse, { + const credential = await this.handleCredentialResponse(agentContext, credentialResponse, { verifyCredentialStatus: options.verifyCredentialStatus, }) - receivedCredentials.push(storedCredential) + // Create credential record, but we don't store it yet (only after the user has accepted the credential) + const credentialRecord = new W3cCredentialRecord({ + credential, + tags: { + expandedTypes: [], + }, + }) + this.logger.debug('Full credential', credentialRecord) + + if (!isInlineCredentialOffer(offeredCredential)) { + const issuerMetadata = client.endpointMetadata.credentialIssuerMetadata + if (!issuerMetadata) { + // TODO: this should not happen + throw new AriesFrameworkError('Issuer metadata not found') + } + const supportedCredentialMetadata = offeredCredential.credentialSupported + // Set the OpenId4Vc credential metadata and update record + setOpenId4VcCredentialMetadata(credentialRecord, supportedCredentialMetadata, serverMetadata, issuerMetadata) + } + + receivedCredentials.push(credentialRecord) } return receivedCredentials @@ -232,17 +306,17 @@ export class OpenId4VcClientService { proofOfPossessionVerificationMethodResolver: ProofOfPossessionVerificationMethodResolver allowedCredentialFormats: SupportedCredentialFormats[] allowedProofOfPossessionSignatureAlgorithms: JwaSignatureAlgorithm[] - credentialMetadata: CredentialMetadata - credentialType: string + offeredCredentialWithMetadata: OfferedCredentialsWithMetadata } ) { - const { credentialFormat, signatureAlgorithm, supportedDidMethods, supportsAllDidMethods } = - this.getProofOfPossessionRequirements(agentContext, { - credentialType: options.credentialType, - credentialMetadata: options.credentialMetadata, + const { signatureAlgorithm, supportedDidMethods, supportsAllDidMethods } = this.getProofOfPossessionRequirements( + agentContext, + { + offeredCredentialWithMetadata: options.offeredCredentialWithMetadata, allowedCredentialFormats: options.allowedCredentialFormats, allowedProofOfPossessionSignatureAlgorithms: options.allowedProofOfPossessionSignatureAlgorithms, - }) + } + ) const JwkClass = getJwkClassFromJwaSignatureAlgorithm(signatureAlgorithm) @@ -254,13 +328,19 @@ export class OpenId4VcClientService { const supportedVerificationMethods = getSupportedVerificationMethodTypesFromKeyType(JwkClass.keyType) + const format = isInlineCredentialOffer(options.offeredCredentialWithMetadata) + ? options.offeredCredentialWithMetadata.inlineCredentialOffer.format + : options.offeredCredentialWithMetadata.credentialSupported.format + // Now we need to determine the did method and alg based on the cryptographic suite const verificationMethod = await options.proofOfPossessionVerificationMethodResolver({ - credentialFormat, + credentialFormat: format as SupportedCredentialFormats, proofOfPossessionSignatureAlgorithm: signatureAlgorithm, supportedVerificationMethods, keyType: JwkClass.keyType, - credentialType: options.credentialType, + supportedCredentialId: !isInlineCredentialOffer(options.offeredCredentialWithMetadata) + ? options.offeredCredentialWithMetadata.credentialSupported.id + : undefined, supportsAllDidMethods, supportedDidMethods, }) @@ -268,6 +348,9 @@ export class OpenId4VcClientService { // Make sure the verification method uses a supported did method if ( !supportsAllDidMethods && + // If supportedDidMethods is undefined, it means the issuer didn't include the binding methods in the metadata + // The user can still select a verification method, but we can't validate it + supportedDidMethods !== undefined && !supportedDidMethods.find((supportedDidMethod) => verificationMethod.id.startsWith(supportedDidMethod)) ) { const { method } = parseDid(verificationMethod.id) @@ -285,7 +368,125 @@ export class OpenId4VcClientService { ) } - return { verificationMethod, signatureAlgorithm, credentialFormat } + return { verificationMethod, signatureAlgorithm } + } + + // todo https://sphereon.atlassian.net/browse/VDX-184 + /** + * Returns all entries from the credential offer. This includes both 'id' entries that reference a supported credential in the issuer metadata, + * as well as inline credential offers that do not reference a supported credential in the issuer metadata. + */ + private getOfferedCredentials( + credentialOfferRequestWithBaseUrl: CredentialOfferRequestWithBaseUrl + ): Array { + if (credentialOfferRequestWithBaseUrl.version < OpenId4VCIVersion.VER_1_0_11) { + const credentialOffer = + credentialOfferRequestWithBaseUrl.original_credential_offer as CredentialOfferPayloadV1_0_08 + + return typeof credentialOffer.credential_type === 'string' + ? [credentialOffer.credential_type] + : credentialOffer.credential_type + } else { + return credentialOfferRequestWithBaseUrl.credential_offer.credentials + } + } + + /** + * Return a normalized version of the credentials supported by the issuer. Can optionally filter based on the credentials + * that were offered, or the type of credentials that are supported. + * + * + * NOTE: for v1_0-08, a single credential id in the issuer metadata could have multiple formats. When retrieving the + * supported credentials, for v1_0-08, the format is appended to the id if there are multiple formats supported for + * that credential id. E.g. if the issuer metadata for v1_0-08 contains an entry with key `OpenBadgeCredential` and + * the supported formats are `jwt_vc-jsonld` and `ldp_vc`, then the id in the credentials supported will be + * `OpenBadgeCredential-jwt_vc-jsonld` and `OpenBadgeCredential-ldp_vc`, even though the offered credential is simply + * `OpenBadgeCredential`. + * + * NOTE: this method only returns the credentials supported by the issuer metadata. It does not take into account the inline + * credentials offered. Use {@link getOfferedCredentialsWithMetadata} to get both the inline and referenced offered credentials. + */ + private getCredentialsSupported( + client: OpenID4VCIClient, + restrictToOfferIds: boolean, + credentialSupportedId?: string + ): CredentialSupported[] { + const offeredIds = this.getOfferedCredentials(client.credentialOffer).filter( + (c): c is string => typeof c === 'string' + ) + + const credentialSupportedIds = restrictToOfferIds ? offeredIds : undefined + + const credentialsSupported = getSupportedCredentials({ + issuerMetadata: client.endpointMetadata.credentialIssuerMetadata, + version: client.version(), + credentialSupportedIds, + }) + + return credentialSupportedId + ? credentialsSupported.filter( + (credentialSupported) => + credentialSupported.id === credentialSupportedId || + credentialSupported.id === `${credentialSupportedId}-${credentialSupported.format}` + ) + : credentialsSupported + } + + /** + * Returns all entries from the credential offer with the associated metadata resolved. For inline entries, the offered credential object + * is included directly. For 'id' entries, the associated `credentials_supported` object is resolved from the issuer metadata. + * + * NOTE: for v1_0-08, a single credential id in the issuer metadata could have multiple formats. This means that the returned value + * from this method could contain multiple entries for a single credential id, but with different formats. This is detectable as the + * id will be the `-`. + */ + private getOfferedCredentialsWithMetadata = (client: OpenID4VCIClient) => { + const offeredCredentials: Array = [] + + for (const offeredCredential of this.getOfferedCredentials(client.credentialOffer)) { + // If the offeredCredential is a string, it references a supported credential in the issuer metadata + if (typeof offeredCredential === 'string') { + const credentialsSupported = this.getCredentialsSupported(client, false, offeredCredential) + + // Make sure the issuer metadata includes the offered credential. + if (credentialsSupported.length === 0) { + throw new Error( + `Offered credential '${offeredCredential}' is not present in the credentials_supported of the issuer metadata` + ) + } + + offeredCredentials.push( + ...credentialsSupported.map((credentialSupported) => { + return { credentialSupported, type: OfferedCredentialType.CredentialSupported } as const + }) + ) + } + // Otherwise it's an inline credential offer that does not reference a supported credential in the issuer metadata + else { + // TODO: we could transform the inline offer to the `CredentialSupported` format, but we'll only be able to populate + // the `format`, `types` and `@context` fields. It's not really clear how to determine the supported did methods, + // signature suites, etc.. for these inline credentials. + // We should also add a property to indicate to the user that this is an inline credential offer. + // if (offeredCredential.format === 'jwt_vc_json') { + // const supported = { + // format: offeredCredential.format, + // types: offeredCredential.types, + // } satisfies CredentialSupportedJwtVcJson; + // } else if (offeredCredential.format === 'jwt_vc_json-ld' || offeredCredential.format === 'ldp_vc') { + // const supported = { + // format: offeredCredential.format, + // '@context': offeredCredential.credential_definition['@context'], + // types: offeredCredential.credential_definition.types, + // } satisfies CredentialSupported; + // } + offeredCredentials.push({ + inlineCredentialOffer: offeredCredential, + type: OfferedCredentialType.InlineCredentialOffer, + } as const) + } + } + + return offeredCredentials } /** @@ -298,48 +499,69 @@ export class OpenId4VcClientService { agentContext: AgentContext, options: { allowedCredentialFormats: SupportedCredentialFormats[] - credentialMetadata: CredentialMetadata + offeredCredentialWithMetadata: OfferedCredentialsWithMetadata allowedProofOfPossessionSignatureAlgorithms: JwaSignatureAlgorithm[] - credentialType: string } ): ProofOfPossessionRequirements { - // Find the potential credentialFormat to use - const potentialCredentialFormats = options.allowedCredentialFormats.filter( - (allowedFormat) => options.credentialMetadata.formats[allowedFormat] !== undefined - ) + const { offeredCredentialWithMetadata, allowedCredentialFormats } = options - // TODO: we may want to add a logging statement here if the supported formats of the wallet - // DOES support one of the issuer formats, but it is not in the allowedFormats - if (potentialCredentialFormats.length === 0) { - const formatsString = Object.keys(options.credentialMetadata.formats).join(', ') - throw new AriesFrameworkError( - `Issuer only supports formats '${formatsString}' for credential type '${ - options.credentialType - }', but the wallet only allows formats '${options.allowedCredentialFormats.join(', ')}'` - ) - } + // Extract format from offer + let format = + offeredCredentialWithMetadata.type === OfferedCredentialType.InlineCredentialOffer + ? offeredCredentialWithMetadata.inlineCredentialOffer.format + : offeredCredentialWithMetadata.credentialSupported.format + + // Get uniform format, so we don't have to deal with the different spec versions + format = getUniformFormat(format) + + const credentialMetadata = + offeredCredentialWithMetadata.type === OfferedCredentialType.CredentialSupported + ? offeredCredentialWithMetadata.credentialSupported + : undefined - // Loop through all the potential credential formats and find the first one that we have a matching - // cryptographic suite supported for. - for (const potentialCredentialFormat of potentialCredentialFormats) { - const credentialFormat = options.credentialMetadata.formats[potentialCredentialFormat] - const issuerSupportedCryptographicSuites = credentialFormat.cryptographic_suites_supported ?? [] + const issuerSupportedCryptographicSuites = credentialMetadata?.cryptographic_suites_supported + const issuerSupportedBindingMethods = + credentialMetadata?.cryptographic_binding_methods_supported ?? // FIXME: somehow the MATTR Launchpad returns binding_methods_supported instead of cryptographic_binding_methods_supported - const issuerSupportedBindingMethods: string[] = - credentialFormat.cryptographic_binding_methods_supported ?? credentialFormat.binding_methods_supported ?? [] + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + (credentialMetadata?.binding_methods_supported as string[] | undefined) + + if (!isInlineCredentialOffer(offeredCredentialWithMetadata)) { + const credentialMetadata = offeredCredentialWithMetadata.credentialSupported + if (!allowedCredentialFormats.includes(format as SupportedCredentialFormats)) { + throw new AriesFrameworkError( + `Issuer only supports format '${format}' for credential type '${ + credentialMetadata.id as string + }', but the wallet only allows formats '${options.allowedCredentialFormats.join(', ')}'` + ) + } + } - // For each of the supported algs, find the key types, then find the proof types - const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) + // For each of the supported algs, find the key types, then find the proof types + const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) - let potentialSignatureAlgorithm: JwaSignatureAlgorithm | undefined + let potentialSignatureAlgorithm: JwaSignatureAlgorithm | undefined - switch (potentialCredentialFormat) { - case ClaimFormat.JwtVc: + switch (format) { + case 'jwt_vc_json': + case 'jwt_vc_json-ld': + // If undefined, it means the issuer didn't include the cryptographic suites in the metadata + // We just guess that the first one is supported + if (issuerSupportedCryptographicSuites === undefined) { + potentialSignatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms[0] + } else { potentialSignatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms.find((signatureAlgorithm) => issuerSupportedCryptographicSuites.includes(signatureAlgorithm) ) - break - case ClaimFormat.LdpVc: + } + break + case 'ldp_vc': + // If undefined, it means the issuer didn't include the cryptographic suites in the metadata + // We just guess that the first one is supported + if (issuerSupportedCryptographicSuites === undefined) { + potentialSignatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms[0] + } else { // We need to find it based on the JSON-LD proof type potentialSignatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms.find( (signatureAlgorithm) => { @@ -353,29 +575,32 @@ export class OpenId4VcClientService { return issuerSupportedCryptographicSuites.includes(matchingSuite.proofType) } ) - break - } - - // If no match, continue to the next one. - if (!potentialSignatureAlgorithm) continue - - const supportsAllDidMethods = issuerSupportedBindingMethods.includes('did') - const supportedDidMethods = issuerSupportedBindingMethods.filter((method) => method.startsWith('did:')) + } + break + default: + throw new AriesFrameworkError( + `Unsupported requested credential format '${format}' with id ${ + credentialMetadata?.id ?? 'Inline credential offer' + }` + ) + } - // Make sure that the issuer supports the 'did' binding method, or at least one specific did method - if (!supportsAllDidMethods && supportedDidMethods.length === 0) continue + const supportsAllDidMethods = issuerSupportedBindingMethods?.includes('did') ?? false + const supportedDidMethods = issuerSupportedBindingMethods?.filter((method) => method.startsWith('did:')) - return { - credentialFormat: potentialCredentialFormat, - signatureAlgorithm: potentialSignatureAlgorithm, - supportedDidMethods, - supportsAllDidMethods, - } + if (!potentialSignatureAlgorithm) { + throw new AriesFrameworkError( + `Could not establish signature algorithm for format ${format} and id ${ + credentialMetadata?.id ?? 'Inline credential offer' + }` + ) } - throw new AriesFrameworkError( - 'Could not determine the correct credential format and signature algorithm to use for the proof of possession.' - ) + return { + signatureAlgorithm: potentialSignatureAlgorithm, + supportedDidMethods, + supportsAllDidMethods, + } } /** @@ -414,15 +639,18 @@ export class OpenId4VcClientService { throw new AriesFrameworkError('Did not receive a successful credential response') } + const format = getUniformFormat(credentialResponse.successBody.format) + const difClaimFormat = fromOpenIdCredentialFormatProfileToDifClaimFormat(format as OpenIdCredentialFormatProfile) + let credential: W3cVerifiableCredential let result: W3cVerifyCredentialResult - if (credentialResponse.successBody.format === ClaimFormat.LdpVc) { + if (difClaimFormat === ClaimFormat.LdpVc) { credential = JsonTransformer.fromJSON(credentialResponse.successBody.credential, W3cJsonLdVerifiableCredential) result = await this.w3cCredentialService.verifyCredential(agentContext, { credential, verifyCredentialStatus: options.verifyCredentialStatus, }) - } else if (credentialResponse.successBody.format === ClaimFormat.JwtVc) { + } else if (difClaimFormat === ClaimFormat.JwtVc) { credential = W3cJwtVerifiableCredential.fromSerializedJwt(credentialResponse.successBody.credential as string) result = await this.w3cCredentialService.verifyCredential(agentContext, { credential, @@ -433,20 +661,17 @@ export class OpenId4VcClientService { } if (!result || !result.isValid) { - throw new AriesFrameworkError(`Failed to validate credential, error = ${result.error}`) + agentContext.config.logger.error('Failed to validate credential', { + result, + }) + throw new AriesFrameworkError(`Failed to validate credential, error = ${result.error?.message ?? 'Unknown'}`) } - const storedCredential = await this.w3cCredentialService.storeCredential(agentContext, { - credential, - }) - this.logger.info(`Stored credential with id: ${storedCredential.id}`) - this.logger.debug('Full credential', storedCredential) - - return storedCredential + return credential } private signCallback(agentContext: AgentContext, verificationMethod: VerificationMethod) { - return async (jwt: Jwt, kid: string) => { + return async (jwt: Jwt, kid?: string) => { if (!jwt.header) { throw new AriesFrameworkError('No header present on JWT') } @@ -455,6 +680,10 @@ export class OpenId4VcClientService { throw new AriesFrameworkError('No payload present on JWT') } + if (!kid) { + throw new AriesFrameworkError('No KID is present in the callback') + } + // We have determined the verification method before and already passed that when creating the callback, // however we just want to make sure that the kid matches the verification method id if (verificationMethod.id !== kid) { @@ -486,3 +715,10 @@ export class OpenId4VcClientService { } } } + +function isInlineCredentialOffer(offeredCredential: OfferedCredentialsWithMetadata): offeredCredential is { + inlineCredentialOffer: CredentialOfferFormat + type: OfferedCredentialType.InlineCredentialOffer +} { + return offeredCredential.type === OfferedCredentialType.InlineCredentialOffer +} diff --git a/packages/openid4vc-client/src/OpenId4VcClientServiceOptions.ts b/packages/openid4vc-client/src/OpenId4VcClientServiceOptions.ts index 8bd580a863..ed03c68765 100644 --- a/packages/openid4vc-client/src/OpenId4VcClientServiceOptions.ts +++ b/packages/openid4vc-client/src/OpenId4VcClientServiceOptions.ts @@ -1,12 +1,16 @@ import type { JwaSignatureAlgorithm, KeyType, VerificationMethod } from '@aries-framework/core' -import { ClaimFormat } from '@aries-framework/core' +import { OpenIdCredentialFormatProfile } from './utils/claimFormatMapping' /** * The credential formats that are supported by the openid4vc client */ -export type SupportedCredentialFormats = ClaimFormat.JwtVc | ClaimFormat.LdpVc -export const supportedCredentialFormats = [ClaimFormat.JwtVc, ClaimFormat.LdpVc] satisfies SupportedCredentialFormats[] +export type SupportedCredentialFormats = OpenIdCredentialFormatProfile.JwtVcJson | OpenIdCredentialFormatProfile.LdpVc + +export const supportedCredentialFormats = [ + OpenIdCredentialFormatProfile.JwtVcJson, + OpenIdCredentialFormatProfile.LdpVc, +] satisfies OpenIdCredentialFormatProfile[] /** * Options that are used for the pre-authorized code flow. @@ -108,8 +112,12 @@ export interface ProofOfPossessionVerificationMethodResolverOptions { /** * The credential type that will be requested from the issuer. This is * based on the credential types that are included the credential offer. + * + * If the offered credential is an inline credential offer, the value + * will be `undefined`. */ - credentialType: string + // TODO: do we need credentialType here? + supportedCredentialId?: string /** * Whether the issuer supports the `did` cryptographic binding method, @@ -128,8 +136,16 @@ export interface ProofOfPossessionVerificationMethodResolverOptions { * MUST be based on one of these did methods. * * The did methods are returned in the format `did:`, e.g. `did:web`. + * + * The value is undefined in the case the supported did methods could not be extracted. + * This is the case when an inline credential was used, or when the issuer didn't include + * the supported did methods in the issuer metadata. + * + * NOTE: an empty array (no did methods supported) has a different meaning from the value + * being undefined (the supported did methods could not be extracted). If `supportsAllDidMethods` + * is true, the value of this property MUST be ignored. */ - supportedDidMethods: string[] + supportedDidMethods?: string[] } /** @@ -145,9 +161,8 @@ export type ProofOfPossessionVerificationMethodResolver = ( * @internal */ export interface ProofOfPossessionRequirements { - credentialFormat: SupportedCredentialFormats signatureAlgorithm: JwaSignatureAlgorithm - supportedDidMethods: string[] + supportedDidMethods?: string[] supportsAllDidMethods: boolean } diff --git a/packages/openid4vc-client/src/index.ts b/packages/openid4vc-client/src/index.ts index 1ca13fe3b1..f6e1e75c5d 100644 --- a/packages/openid4vc-client/src/index.ts +++ b/packages/openid4vc-client/src/index.ts @@ -1,14 +1,22 @@ -export * from './OpenId4VcClientModule' +import 'fast-text-encoding' + export * from './OpenId4VcClientApi' +export * from './OpenId4VcClientModule' export * from './OpenId4VcClientService' - // Contains internal types, so we don't export everything export { AuthCodeFlowOptions, - PreAuthCodeFlowOptions, GenerateAuthorizationUrlOptions, - RequestCredentialOptions, - SupportedCredentialFormats, + PreAuthCodeFlowOptions, ProofOfPossessionVerificationMethodResolver, ProofOfPossessionVerificationMethodResolverOptions, + RequestCredentialOptions, + SupportedCredentialFormats, } from './OpenId4VcClientServiceOptions' +export * from './presentations' +export { + getOpenId4VcCredentialMetadata, + OpenId4VcCredentialMetadata, + OpenIdCredentialFormatProfile, + setOpenId4VcCredentialMetadata, +} from './utils' diff --git a/packages/openid4vc-client/src/presentations/OpenId4VpClientService.ts b/packages/openid4vc-client/src/presentations/OpenId4VpClientService.ts new file mode 100644 index 0000000000..0c8d98dbcd --- /dev/null +++ b/packages/openid4vc-client/src/presentations/OpenId4VpClientService.ts @@ -0,0 +1,213 @@ +import type { AgentContext, W3cVerifiableCredential, W3cVerifiablePresentation } from '@aries-framework/core' +import type { + DIDDocument, + PresentationDefinitionWithLocation, + SigningAlgo, + URI, + Verification, + VerifiedAuthorizationRequest, +} from '@sphereon/did-auth-siop' +import type { PresentationDefinitionV1 } from '@sphereon/pex-models' +import type { W3CVerifiablePresentation } from '@sphereon/ssi-types' + +import { + AriesFrameworkError, + Buffer, + DidsApi, + getJwkClassFromKeyType, + getKeyFromVerificationMethod, + injectable, + TypedArrayEncoder, + W3cJsonLdVerifiablePresentation, + asArray, +} from '@aries-framework/core' +import { CheckLinkedDomain, OP, ResponseMode, SupportedVersion, VerificationMode } from '@sphereon/did-auth-siop' + +import { PresentationExchangeService } from './PresentationExchangeService' + +/** + * SIOPv2 Authorization Request with a single v1 presentation definition + */ +export type VerifiedAuthorizationRequestWithPresentationDefinition = VerifiedAuthorizationRequest & { + presentationDefinitions: [PresentationDefinitionWithLocation & { definition: PresentationDefinitionV1 }] +} + +function isVerifiedAuthorizationRequestWithPresentationDefinition( + request: VerifiedAuthorizationRequest +): request is VerifiedAuthorizationRequestWithPresentationDefinition { + return ( + request.presentationDefinitions !== undefined && + request.presentationDefinitions.length === 1 && + request.presentationDefinitions?.[0]?.definition !== undefined + ) +} + +@injectable() +export class OpenId4VpClientService { + public constructor(private presentationExchangeService: PresentationExchangeService) {} + + private getOp(agentContext: AgentContext) { + const supportedDidMethods = this.getSupportedDidMethods(agentContext) + + const builder = OP.builder() + .withResponseMode(ResponseMode.POST) + .withSupportedVersions([SupportedVersion.SIOPv2_ID1]) + .withExpiresIn(300) + .withCheckLinkedDomain(CheckLinkedDomain.NEVER) + .withCustomResolver(this.getResolver(agentContext)) + + // Add did methods + for (const supportedDidMethod of supportedDidMethods) { + builder.addDidMethod(supportedDidMethod) + } + + const op = builder.build() + + return op + } + + public async selectCredentialForProofRequest( + agentContext: AgentContext, + options: { + authorizationRequest: string | URI + } + ) { + const op = this.getOp(agentContext) + + const verification = { + mode: VerificationMode.EXTERNAL, + resolveOpts: { + resolver: this.getResolver(agentContext), + noUniversalResolverFallback: true, + }, + } satisfies Verification + + // FIXME: this uses did-jwt for verification of the JWT, we can't verify it ourselves. + const verifiedAuthorizationRequest = await op.verifyAuthorizationRequest(options.authorizationRequest, { + verification, + }) + + if (!isVerifiedAuthorizationRequestWithPresentationDefinition(verifiedAuthorizationRequest)) { + throw new AriesFrameworkError( + 'Only SIOPv2 authorization request including a single presentation definition are supported' + ) + } + + const selectResults = await this.presentationExchangeService.selectCredentialsForRequest( + agentContext, + verifiedAuthorizationRequest.presentationDefinitions[0].definition + ) + + return { + verifiedAuthorizationRequest, + selectResults, + } + } + + /** + * Send a SIOPv2 authentication response to the relying party including a verifiable + * presentation based on OpenID4VP. + */ + public async shareProof( + agentContext: AgentContext, + options: { + verifiedAuthorizationRequest: VerifiedAuthorizationRequestWithPresentationDefinition + selectedCredentials: W3cVerifiableCredential[] + } + ) { + const op = this.getOp(agentContext) + + const vp = await this.presentationExchangeService.createPresentation(agentContext, { + selectedCredentials: options.selectedCredentials, + presentationDefinition: options.verifiedAuthorizationRequest.presentationDefinitions[0].definition, + // TODO: challenge / nonce + }) + + const verificationMethod = await this.getVerificationMethodFromVerifiablePresentation( + agentContext, + vp.verifiablePresentation + ) + const key = getKeyFromVerificationMethod(verificationMethod) + const alg = getJwkClassFromKeyType(key.keyType)?.supportedSignatureAlgorithms[0] + if (!alg) { + throw new AriesFrameworkError(`No supported algs for key type: ${key.keyType}`) + } + + const response = await op.createAuthorizationResponse(options.verifiedAuthorizationRequest, { + presentationExchange: { + verifiablePresentations: [vp.verifiablePresentation.encoded as W3CVerifiablePresentation], + presentationSubmission: vp.presentationSubmission, + }, + signature: { + signature: async (data) => { + const signature = await agentContext.wallet.sign({ + data: typeof data === 'string' ? TypedArrayEncoder.fromString(data) : Buffer.from(data), + key, + }) + + return TypedArrayEncoder.toBase64URL(signature) + }, + // FIXME: cast + alg: alg as unknown as SigningAlgo, + did: verificationMethod.controller, + kid: verificationMethod.id, + }, + }) + + const responseToResponse = await op.submitAuthorizationResponse(response) + + if (!responseToResponse.ok) { + throw new AriesFrameworkError(`Error submitting authorization response. ${await responseToResponse.text()}`) + } + } + + private getSupportedDidMethods(agentContext: AgentContext) { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const supportedDidMethods: string[] = [] + + for (const resolver of didsApi.config.resolvers) { + supportedDidMethods.push(...resolver.supportedMethods) + } + + return supportedDidMethods + } + + private getResolver(agentContext: AgentContext) { + return { + resolve: async (didUrl: string) => { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const result = await didsApi.resolve(didUrl) + + return { + ...result, + didDocument: result.didDocument?.toJSON() as DIDDocument, + } + }, + } + } + + // TODO: we can do this in a simpler way, as we're now resolving it multiple times + private async getVerificationMethodFromVerifiablePresentation( + agentContext: AgentContext, + verifiablePresentation: W3cVerifiablePresentation + ) { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + + let verificationMethod: string + if (verifiablePresentation instanceof W3cJsonLdVerifiablePresentation) { + const [firstProof] = asArray(verifiablePresentation.proof) + + if (!firstProof) { + throw new AriesFrameworkError('Verifiable presentation does not contain a proof') + } + verificationMethod = firstProof.verificationMethod + } else { + // FIXME: cast + verificationMethod = verifiablePresentation.jwt.header.kid as string + } + + const didDocument = await didsApi.resolveDidDocument(verificationMethod) + + return didDocument.dereferenceKey(verificationMethod, ['authentication']) + } +} diff --git a/packages/openid4vc-client/src/presentations/PresentationExchangeService.ts b/packages/openid4vc-client/src/presentations/PresentationExchangeService.ts new file mode 100644 index 0000000000..afa8af64a7 --- /dev/null +++ b/packages/openid4vc-client/src/presentations/PresentationExchangeService.ts @@ -0,0 +1,247 @@ +import type { PresentationSubmission } from './selection/types' +import type { + AgentContext, + Query, + VerificationMethod, + W3cCredentialRecord, + W3cVerifiableCredential, +} from '@aries-framework/core' +import type { PresentationSignCallBackParams } from '@sphereon/pex' +import type { PresentationDefinitionV1 } from '@sphereon/pex-models' +import type { IVerifiablePresentation } from '@sphereon/ssi-types' + +import { + AriesFrameworkError, + ClaimFormat, + DidsApi, + getJwkFromKey, + getKeyFromVerificationMethod, + injectable, + JsonTransformer, + utils, + W3cCredentialService, + W3cPresentation, + W3cCredentialRepository, +} from '@aries-framework/core' +import { PEXv1, Status } from '@sphereon/pex' + +import { selectCredentialsForRequest } from './selection/PexCredentialSelection' +import { + getSphereonW3cVerifiableCredential, + getSphereonW3cVerifiablePresentation, + getW3cVerifiablePresentationInstance, +} from './transform' + +@injectable() +export class PresentationExchangeService { + private pex = new PEXv1() + + /** + * Validates a DIF Presentation Definition + */ + public validateDefinition(presentationDefinition: PresentationDefinitionV1) { + const result = PEXv1.validateDefinition(presentationDefinition) + + // check if error + const firstResult = Array.isArray(result) ? result[0] : result + + if (firstResult.status !== Status.INFO) { + throw new AriesFrameworkError( + `Error in presentation exchange presentationDefinition: ${firstResult?.message ?? 'Unknown'} ` + ) + } + } + + public evaluatePresentation({ + presentationDefinition, + presentation, + }: { + presentationDefinition: PresentationDefinitionV1 + presentation: IVerifiablePresentation + }) { + // validate contents of presentation + const evaluationResults = this.pex.evaluatePresentation(presentationDefinition, presentation) + + return evaluationResults + } + + public async selectCredentialsForRequest( + agentContext: AgentContext, + presentationDefinition: PresentationDefinitionV1 + ): Promise { + const credentialRecords = await this.queryCredentialForPresentationDefinition(agentContext, presentationDefinition) + + return selectCredentialsForRequest(presentationDefinition, credentialRecords) + } + + public async createPresentation( + agentContext: AgentContext, + { + selectedCredentials, + presentationDefinition, + challenge, + domain, + }: { + selectedCredentials: W3cVerifiableCredential[] + presentationDefinition: PresentationDefinitionV1 + challenge?: string + domain?: string + } + ) { + if (selectedCredentials.length === 0) { + throw new AriesFrameworkError('No credentials selected for creating presentation.') + } + + // We use the subject id to resolve the DID document. + // I am assuming the subject is the same for all credentials (for now) + // The presentation contains multiple credentials and these are being added + // TODO how do we derive the verification method if there are multiple subject Ids + // FIXME + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const [firstSubjectId] = selectedCredentials[0]?.credentialSubjectIds ?? [] + + // Credential is allowed to be presented without a subject id. In that case we can't prove ownership of credential + // And it is more like a bearer token. + // In the future we can first check the holder key and if it exists we can use that as the one that should authenticate + // https://www.w3.org/TR/vc-data-model/#example-a-credential-issued-to-a-holder-who-is-not-the-only-subject-of-the-credential-who-has-no-relationship-with-the-subject-of-the-credential-but-who-has-a-relationship-with-the-issuer + if (!firstSubjectId) { + throw new AriesFrameworkError( + 'Credential subject missing from the selected credential for creating presentation.' + ) + } + + // Determine a suitable verification method for the presentation + const verificationMethod = await this.getVerificationMethodForSubjectId(agentContext, firstSubjectId) + + if (!verificationMethod) { + throw new AriesFrameworkError(`No verification method found for subject id ${firstSubjectId}`) + } + + // Q1: is holder always subject id, what if there are multiple subjects??? + // Q2: What about proofType, proofPurpose verification method for multiple subjects? + const verifiablePresentationResult = await this.pex.verifiablePresentationFrom( + presentationDefinition, + selectedCredentials.map(getSphereonW3cVerifiableCredential), + this.getPresentationSignCallback(agentContext, verificationMethod), + { + holderDID: firstSubjectId, + proofOptions: { + challenge, + domain, + // TODO: add nonce + }, + signatureOptions: { + verificationMethod: verificationMethod?.id, + }, + } + ) + + return { + verifiablePresentation: getW3cVerifiablePresentationInstance(verifiablePresentationResult.verifiablePresentation), + presentationSubmission: verifiablePresentationResult.presentationSubmission, + presentationSubmissionLocation: verifiablePresentationResult.presentationSubmissionLocation, + } + } + + public getPresentationSignCallback(agentContext: AgentContext, verificationMethod: VerificationMethod) { + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + + return async (callBackParams: PresentationSignCallBackParams) => { + // The created partial proof and presentation, as well as original supplied options + const { presentation: presentationJson, options } = callBackParams + const { challenge, domain, nonce } = options.proofOptions ?? {} + const { verificationMethod: verificationMethodId } = options.signatureOptions ?? {} + + const w3cPresentation = JsonTransformer.fromJSON(presentationJson, W3cPresentation) + + if (verificationMethodId && verificationMethodId !== verificationMethod.id) { + throw new AriesFrameworkError( + `Verification method from signing options ${verificationMethodId} does not match verification method ${verificationMethod.id}.` + ) + } + + // NOTE: we currently don't support mixed presentations, where some credentials + // are JWT and some are JSON-LD. It could be however that the presentation contains + // some JWT and some JSON-LD credentials. (for DDIP we only support JWT, so we should be fine) + const isJwt = typeof presentationJson.verifiableCredential?.[0] === 'string' + + if (!isJwt) { + throw new AriesFrameworkError(`Only JWT credentials are supported for presentation exchange.`) + } + + const key = getKeyFromVerificationMethod(verificationMethod) + const jwk = getJwkFromKey(key) + + const alg = jwk.supportedSignatureAlgorithms[0] + if (!alg) { + throw new AriesFrameworkError(`No supported algs for key type: ${key.keyType}`) + } + + const signedPresentation = await w3cCredentialService.signPresentation(agentContext, { + format: ClaimFormat.JwtVp, + verificationMethod: verificationMethod.id, + presentation: w3cPresentation, + alg, + challenge: challenge ?? nonce ?? utils.uuid(), + domain, + }) + + return getSphereonW3cVerifiablePresentation(signedPresentation) + } + } + + private async getVerificationMethodForSubjectId(agentContext: AgentContext, subjectId: string) { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + + if (!subjectId.startsWith('did:')) { + throw new AriesFrameworkError(`Only dids are supported as credentialSubject id. ${subjectId} is not a valid did`) + } + + const didDocument = await didsApi.resolveDidDocument(subjectId) + + if (!didDocument.authentication || didDocument.authentication.length === 0) { + throw new AriesFrameworkError(`No authentication verificationMethods found for did ${subjectId} in did document`) + } + + // the signature suite to use for the presentation is dependant on the credentials we share. + // 1. Get the verification method for this given proof purpose in this DID document + let [verificationMethod] = didDocument.authentication + if (typeof verificationMethod === 'string') { + verificationMethod = didDocument.dereferenceKey(verificationMethod, ['authentication']) + } + + return verificationMethod + } + + /** + * Queries the wallet for credentials that match the given presentation definition. This only does an initial query based on the + * schema of the input descriptors. It does not do any further filtering based on the constraints in the input descriptors. + */ + private async queryCredentialForPresentationDefinition( + agentContext: AgentContext, + presentationDefinition: PresentationDefinitionV1 + ) { + const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + + const query: Array> = [] + + // The schema.uri can contain either an expanded type, or a context uri + for (const inputDescriptor of presentationDefinition.input_descriptors) { + for (const schema of inputDescriptor.schema) { + // FIXME: It's currently not possible to query by the `type` of the credential. So we fetch all JWT VCs for now + query.push({ + $or: [{ expandedType: [schema.uri] }, { contexts: [schema.uri] }, { claimFormat: ClaimFormat.JwtVc }], + }) + } + } + + // query the wallet ourselves first to avoid the need to query the pex library for all + // credentials for every proof request + const credentialRecords = await w3cCredentialRepository.findByQuery(agentContext, { + $or: query, + }) + + return credentialRecords + } +} diff --git a/packages/openid4vc-client/src/presentations/example.md b/packages/openid4vc-client/src/presentations/example.md new file mode 100644 index 0000000000..2a0f170c2e --- /dev/null +++ b/packages/openid4vc-client/src/presentations/example.md @@ -0,0 +1,66 @@ +# Presentation Submission Example + +This document gives an example of the result returned by `PresentationExchangeService.selectCredentialsForRequest`. + +On startup of the agent if the wallet does not have a DBC credential yet, it will be added. In the `WalletScreen` I'v added an useEffect with a note that should how you can get the below example results for rendering. There's no way to submit yet, but this is enough to render everything for the proof request. + +### Request can be satisfied + +The following value represents a presentation that can be satisfied using the following submission. The value `areRequirementsSatisfied: true` indicates that all requirements are met. + +Each requirement can contain 1 to N `submissions` entries, where each submission contributes to the requirement. If `isRequirementSatisfied` is `true`, you can render all `submission` entries as a credential on the proof share page. + +Each requirement represents a different group (not sure if we want to show this as separate groups, but each group can have a `name` and `purpose`) + +```json +{ + "areRequirementsSatisfied": true, + "requirements": [ + { + "isRequirementSatisfied": true, + "needsCount": 1, + "submission": [ + { + "inputDescriptorId": "c2834d0e-3c95-4721-b21a-40e3d7ea2549", + "name": "DBC Conference 2023 Attendee", + "purpose": "To access this portal your DBC Conference 2023 attendance proof is required.", + "verifiableCredential": + } + ] + } + ], + "purpose": "We want to know your name and e-mail address (will not be stored)" +} +``` + +### Request could not be satisfied. + +The example does not satisfy the requirements. As you can see in `areRequirementsSatisfied: false`. If this is the case you need to loop through all the requirement and for each requirement determine whether the requirement is satisfied (`isRequirementSatisfied: true`). If this is the case you can render the submission entries as is with the succesfull case above. It there's a requirement that is not satisfied (`isRequirementSatisfied: false`), the submission entries will contain a list of submission that entries **that could satisfy the requirement**. However there will be entries where the `verifiableCredential` value is `undefined`. + +An example is a requirement that has `needsCount: 3`, but there's only 2 submission entries that could be satisfied. The `submission` list can have a length of 4 . In this case the verifier says: Here's 4 requirements, you can choose any 3 (indicated by `needsCount`) of these submission possiblities. If two submission entries could be satisfied, there will be a list of 4 submission entries, where 2 of them have a `verifiableCredential` value of `undefined`, and two will have a `verifiableCredential` value that can be rendered. This allows the wallet to say you have 2 credentials, but you are missing 1 of these two (and those could include the `name` and `purpose` of that submission so user knows what they need to get to satisfy this request). This is maybe overly complex for now, but so you have at least at the information that is available to show the user exactly why a presentation can't be satisfied. If you want you could also just check `areRequirementsSatisfied: true` and show a general error screen otherwise, but this gives the user less info about what went wrong. + +```jsonc +{ + "areRequirementsSatisfied": false, + "requirements": [ + { + "isRequirementSatisfied": false, + "submission": [ + { + "inputDescriptorId": "c2834d0e-3c95-4721-b21a-40e3d7ea2549", + "name": "DBC Conference 2023 Attendee", + "purpose": "To access this portal your DBC Conference 2023 attendance proof is required.", + "verifiableCredential": + }, + { + "inputDescriptorId": "c2834d0e-3c95-4721-b21a-40e3d7ea2549", + "name": "Not Present", + "purpose": "We want a credential you don't have" + } + ], + "needsCount": 2 + } + ], + "purpose": "We want to know your name and e-mail address (will not be stored)" +} +``` diff --git a/packages/openid4vc-client/src/presentations/fixtures.ts b/packages/openid4vc-client/src/presentations/fixtures.ts new file mode 100644 index 0000000000..8fa169a956 --- /dev/null +++ b/packages/openid4vc-client/src/presentations/fixtures.ts @@ -0,0 +1,76 @@ +import type { PresentationDefinitionV1 } from '@sphereon/pex-models' + +export const multipleCredentialPresentationDefinition: PresentationDefinitionV1 = { + id: '022c2664-68cc-45cc-b291-789ce8b599eb', + purpose: 'We want to know your name and e-mail address (will not be stored)', + input_descriptors: [ + { + id: 'c2834d0e-3c95-4721-b21a-40e3d7ea2549', + name: 'DBC Conference 2023 Attendee', + purpose: 'To access this portal your DBC Conference 2023 attendance proof is required.', + group: ['A'], + schema: [ + { + uri: 'DBCConferenceAttendee', + required: true, + }, + ], + constraints: { + fields: [ + { + path: ['$.credentialSubject.event.name', '$.vc.credentialSubject.event.name'], + filter: { + type: 'string', + pattern: 'DBC Conference 2023', + }, + }, + ], + }, + }, + { + id: 'c2834d0e-3c95-4721-b21a-40e3d7ea2549', + name: 'Drivers licence', + purpose: + 'Your drivers license is needed to validate your birth date. We do this to prevent fraud with conference tickets.', + group: ['A'], + schema: [ + { + uri: 'NotPresent', + required: true, + }, + ], + }, + ], + submission_requirements: [ + { + rule: 'pick', + count: 2, + from: 'A', + }, + ], +} + +export const dbcPresentationDefinition: PresentationDefinitionV1 = { + id: '022c2664-68cc-45cc-b291-789ce8b599eb', + purpose: 'We want to know your name and e-mail address (will not be stored)', + input_descriptors: [ + { + id: 'c2834d0e-3c95-4721-b21a-40e3d7ea2549', + name: 'DBC Conference 2023 Attendee', + purpose: 'To access this portal your DBC Conference 2023 attendance proof is required.', + group: ['A'], + schema: [ + { + uri: 'DBCConferenceAttendee', + required: true, + }, + ], + }, + ], + submission_requirements: [ + { + rule: 'all', + from: 'A', + }, + ], +} diff --git a/packages/openid4vc-client/src/presentations/index.ts b/packages/openid4vc-client/src/presentations/index.ts new file mode 100644 index 0000000000..84d9845844 --- /dev/null +++ b/packages/openid4vc-client/src/presentations/index.ts @@ -0,0 +1,6 @@ +export { + OpenId4VpClientService, + VerifiedAuthorizationRequestWithPresentationDefinition, +} from './OpenId4VpClientService' +export { PresentationExchangeService } from './PresentationExchangeService' +export { PresentationSubmission, SubmissionEntry } from './selection' diff --git a/packages/openid4vc-client/src/presentations/selection/PexCredentialSelection.ts b/packages/openid4vc-client/src/presentations/selection/PexCredentialSelection.ts new file mode 100644 index 0000000000..630e6cfe06 --- /dev/null +++ b/packages/openid4vc-client/src/presentations/selection/PexCredentialSelection.ts @@ -0,0 +1,303 @@ +import type { PresentationSubmission, PresentationSubmissionRequirement, SubmissionEntry } from './types' +import type { W3cCredentialRecord } from '@aries-framework/core' +import type { SelectResults, SubmissionRequirementMatch } from '@sphereon/pex' +import type { PresentationDefinitionV1, SubmissionRequirement, InputDescriptorV1 } from '@sphereon/pex-models' +import type { OriginalVerifiableCredential } from '@sphereon/ssi-types' + +import { AriesFrameworkError } from '@aries-framework/core' +import { PEXv1 } from '@sphereon/pex' +import { Rules } from '@sphereon/pex-models' +import { default as jp } from 'jsonpath' + +import { getSphereonW3cVerifiableCredential } from '../transform' + +export function selectCredentialsForRequest( + presentationDefinition: PresentationDefinitionV1, + credentialRecords: W3cCredentialRecord[] +): PresentationSubmission { + const pex = new PEXv1() + + const encodedCredentials: OriginalVerifiableCredential[] = credentialRecords.map((c) => + getSphereonW3cVerifiableCredential(c.credential) + ) + + const selectResultsRaw = pex.selectFrom(presentationDefinition, encodedCredentials) + + const selectResults = { + ...selectResultsRaw, + // Map the encoded credential to their respective w3c credential record + verifiableCredential: selectResultsRaw.verifiableCredential?.map((encoded): W3cCredentialRecord => { + const credentialIndex = encodedCredentials.indexOf(encoded) + const credentialRecord = credentialRecords[credentialIndex] + if (!credentialRecord) { + throw new AriesFrameworkError('Unable to find credential in credential records') + } + + return credentialRecord + }), + } + + const presentationSubmission: PresentationSubmission = { + areRequirementsSatisfied: false, + requirements: [], + name: presentationDefinition.name, + purpose: presentationDefinition.purpose, + } + + // If there's no submission requirements, ALL input descriptors MUST be satisfied + if (!presentationDefinition.submission_requirements || presentationDefinition.submission_requirements.length === 0) { + presentationSubmission.requirements = getSubmissionRequirementsAllInputDescriptors( + presentationDefinition, + selectResults + ) + } else { + presentationSubmission.requirements = getSubmissionRequirements(presentationDefinition, selectResults) + } + + // There may be no requirements if we filter out all optional ones. To not makes things too complicated, we see it as an error + // for now if a request is made that has no required requirements (but only e.g. min: 0, which means we don't need to disclose anything) + // I see this more as the fault of the presentation definition, as it should have at least some requirements. + if (presentationSubmission.requirements.length === 0) { + throw new AriesFrameworkError( + 'Presentation Definition does not require any credentials. Optional credentials are not included in the presentation submission.' + ) + } + + return { + ...presentationSubmission, + + // If all requirements are satisfied, the presentation submission is satisfied + areRequirementsSatisfied: presentationSubmission.requirements.every( + (requirement) => requirement.isRequirementSatisfied + ), + } +} + +function getSubmissionRequirements( + presentationDefinition: PresentationDefinitionV1, + selectResults: W3cCredentialRecordSelectResults +): PresentationSubmissionRequirement[] { + const submissionRequirements: PresentationSubmissionRequirement[] = [] + + // There are submission requirements, so we need to select the input_descriptors + // based on the submission requirements + for (const submissionRequirement of presentationDefinition.submission_requirements ?? []) { + // Check if the submissionRequirement uses `from_nested`, as we don't support this yet + if (submissionRequirement.from_nested) { + throw new AriesFrameworkError( + "Presentation definition contains requirement using 'from_nested', which is not supported yet." + ) + } + + // Check if there's a 'from'. If not the structure is not as we expect it + if (!submissionRequirement.from) { + throw new AriesFrameworkError("Missing 'from' in submission requirement match") + } + + // Rule is all + if (submissionRequirement.rule === Rules.All) { + const selectedSubmission = getSubmissionRequirementRuleAll( + submissionRequirement, + presentationDefinition, + selectResults + ) + + // Submission may have requirement that doesn't require a credential to be submitted (e.g. min: 0) + // We use minimization strategy, and thus only disclose the minimum amount of information + // TODO: is this the right place to do this? + if (selectedSubmission.needsCount > 0) { + submissionRequirements.push(selectedSubmission) + } + } + // Rule is Pick + else { + const selectedSubmission = getSubmissionRequirementRulePick( + submissionRequirement, + presentationDefinition, + selectResults + ) + + // Submission may have requirement that doesn't require a credential to be submitted (e.g. min: 0) + // We use minimization strategy, and thus only disclose the minimum amount of information + // TODO: is this the right place to do this? + if (selectedSubmission.needsCount > 0) { + submissionRequirements.push(selectedSubmission) + } + } + } + + return submissionRequirements +} + +function getSubmissionRequirementsAllInputDescriptors( + presentationDefinition: PresentationDefinitionV1, + selectResults: W3cCredentialRecordSelectResults +): PresentationSubmissionRequirement[] { + const submissionRequirements: PresentationSubmissionRequirement[] = [] + + for (const inputDescriptor of presentationDefinition.input_descriptors) { + const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) + + submissionRequirements.push({ + isRequirementSatisfied: submission.verifiableCredential !== undefined, + submission: [submission], + // Every input descriptor is a separate requirement, so the count is always 1 + needsCount: 1, + }) + } + + return submissionRequirements +} + +function getSubmissionRequirementRuleAll( + submissionRequirement: SubmissionRequirement, + presentationDefinition: PresentationDefinitionV1, + selectResults: W3cCredentialRecordSelectResults +) { + // Check if there's a 'from'. If not the structure is not as we expect it + if (!submissionRequirement.from) { + throw new AriesFrameworkError("Missing 'from' in submission requirement match") + } + + const selectedSubmission: PresentationSubmissionRequirement = { + name: submissionRequirement.name, + purpose: submissionRequirement.purpose, + isRequirementSatisfied: false, + needsCount: 0, + submission: [], + } + + for (const inputDescriptor of presentationDefinition.input_descriptors) { + // We only want to get the submission if the input descriptor belongs to the group + if (!inputDescriptor.group?.includes(submissionRequirement.from)) continue + + const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) + + // Rule ALL, so for every input descriptor that matches in this group, we need to add it + selectedSubmission.needsCount += 1 + selectedSubmission.submission.push(submission) + } + + return { + ...selectedSubmission, + + // If all submissions have a credential, the requirement is satisfied + isRequirementSatisfied: selectedSubmission.submission.every( + (submission) => submission.verifiableCredential !== undefined + ), + } +} + +function getSubmissionRequirementRulePick( + submissionRequirement: SubmissionRequirement, + presentationDefinition: PresentationDefinitionV1, + selectResults: W3cCredentialRecordSelectResults +) { + // Check if there's a 'from'. If not the structure is not as we expect it + if (!submissionRequirement.from) { + throw new AriesFrameworkError("Missing 'from' in submission requirement match") + } + + const selectedSubmission: PresentationSubmissionRequirement = { + name: submissionRequirement.name, + purpose: submissionRequirement.purpose, + isRequirementSatisfied: false, + submission: [], + + // TODO: if there's no count, min, max should we then assume the number to include is 1? + // TODO: if there's no count, min, but there is a max. Should we assume the min is 0 or 1? + needsCount: submissionRequirement.count ?? submissionRequirement.min ?? 1, + } + + const satisfiedSubmissions: SubmissionEntry[] = [] + const unsatisfiedSubmissions: SubmissionEntry[] = [] + + for (const inputDescriptor of presentationDefinition.input_descriptors) { + // We only want to get the submission if the input descriptor belongs to the group + if (!inputDescriptor.group?.includes(submissionRequirement.from)) continue + + const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) + + if (submission.verifiableCredential) { + satisfiedSubmissions.push(submission) + } else { + unsatisfiedSubmissions.push(submission) + } + + if (satisfiedSubmissions.length === selectedSubmission.needsCount) { + break + } + } + + return { + ...selectedSubmission, + + // If there's enough satisfied submissions, the requirement is satisfied + isRequirementSatisfied: satisfiedSubmissions.length === selectedSubmission.needsCount, + + // if the requirement is satisfied, we only need to return the satisfied submissions + // however if the requirement is not satisfied, we include all entries so the wallet could + // render which credentials are missing. + submission: + satisfiedSubmissions.length === selectedSubmission.needsCount + ? satisfiedSubmissions + : [...satisfiedSubmissions, ...unsatisfiedSubmissions], + } +} + +function getSubmissionForInputDescriptor( + inputDescriptor: InputDescriptorV1, + selectResults: W3cCredentialRecordSelectResults +): SubmissionEntry { + // https://github.com/Sphereon-Opensource/PEX/issues/116 + // FIXME: the match.name is only the id if the input_descriptor has no name + // Find first match + const match = selectResults.matches?.find( + (m) => + m.name === inputDescriptor.id || + // FIXME: this is not collision proof as the name doesn't have to be unique + m.name === inputDescriptor.name + ) + + const submissionEntry: SubmissionEntry = { + inputDescriptorId: inputDescriptor.id, + name: inputDescriptor.name, + purpose: inputDescriptor.purpose, + } + + // return early if no match. + if (!match) return submissionEntry + + // FIXME: This can return multiple credentials for multiple input_descriptors, + // which I think is a bug in the PEX library + // Extract all credentials from the match + const [verifiableCredential] = extractCredentialsFromMatch(match, selectResults.verifiableCredential) + + return { + ...submissionEntry, + verifiableCredential, + } +} + +function extractCredentialsFromMatch(match: SubmissionRequirementMatch, availableCredentials?: W3cCredentialRecord[]) { + const verifiableCredentials: W3cCredentialRecord[] = [] + + for (const vcPath of match.vc_path) { + const [verifiableCredential] = jp.query( + { + verifiableCredential: availableCredentials, + }, + vcPath + ) as [W3cCredentialRecord] + verifiableCredentials.push(verifiableCredential) + } + + return verifiableCredentials +} + +/** + * Custom SelectResults that include the W3cCredentialRecord instead of the encoded verifiable credential + */ +export type W3cCredentialRecordSelectResults = Omit & { + verifiableCredential?: W3cCredentialRecord[] +} diff --git a/packages/openid4vc-client/src/presentations/selection/index.ts b/packages/openid4vc-client/src/presentations/selection/index.ts new file mode 100644 index 0000000000..9c6bddae57 --- /dev/null +++ b/packages/openid4vc-client/src/presentations/selection/index.ts @@ -0,0 +1,2 @@ +export { selectCredentialsForRequest } from './PexCredentialSelection' +export { PresentationSubmission, PresentationSubmissionRequirement, SubmissionEntry } from './types' diff --git a/packages/openid4vc-client/src/presentations/selection/types.ts b/packages/openid4vc-client/src/presentations/selection/types.ts new file mode 100644 index 0000000000..72088c4ffa --- /dev/null +++ b/packages/openid4vc-client/src/presentations/selection/types.ts @@ -0,0 +1,108 @@ +import type { W3cCredentialRecord } from '@aries-framework/core' + +/** + * A submission entry that satisfies a specific input descriptor from the + * presentation definition. + */ +export interface SubmissionEntry { + /** + * The id of the input descriptor + */ + inputDescriptorId: string + + /** + * Name of the input descriptor + */ + name?: string + + /** + * Purpose of the input descriptor + */ + purpose?: string + + /** + * The verifiable credential that satisfies the input descriptor. + * + * If the value is undefined, it means the input descriptor could + * not be satisfied. + */ + verifiableCredential?: W3cCredentialRecord +} + +/** + * A requirement for the presentation submission. A requirement + * is a group of input descriptors that together fulfill a requirement + * from the presentation definition. + * + * Each submission represents a input descriptor. + */ +export interface PresentationSubmissionRequirement { + /** + * Whether the requirement is satisfied. + * + * If the requirement is not satisfied, the submission will still contain + */ + isRequirementSatisfied: boolean + + /** + * Name of the requirement + */ + name?: string + + /** + * Purpose of the requirement + */ + purpose?: string + + /** + * Array of objects, where each entry contains a credential that will be part + * of the submission. + * + * NOTE: if the `isRequirementSatisfied` is `false` the submission list will + * contain entries without a verifiable credential. In this case it could also + * contain more entries than are actually needed (as you sometimes can choose from + * e.g. 4 types of credentials and need to submit at least two). If + * `isRequirementSatisfied` is `false`, make sure to check the `needsCount` value + * to see how many of those submissions needed. + */ + submission: SubmissionEntry[] + + /** + * The number of submission entries that are needed to fulfill the requirement. + * If `isRequirementSatisfied` is `true`, the submission list will always be equal + * to the number of `needsCount`. If `isRequirementSatisfied` is `false` the list of + * submissions could be longer. + */ + needsCount: number + + // TODO: add requirement/restriction for input +} + +export interface PresentationSubmission { + /** + * Whether all requirements have been satisfied by the credentials in the wallet. + */ + areRequirementsSatisfied: boolean + + /** + * The requirements for the presentation definition. If the `areRequirementsSatisfied` value + * is `false`, this list will still be populated with requirements, but won't contain credentials + * for all requirements. This can be useful to display the missing credentials for a presentation + * definition to be satisfied. + * + * NOTE: Presentation definition requirements can be really complex as there's a lot of different + * combinations that are possible. The structure doesn't include all possible combinations yet that + * could satisfy a presentation definition. + */ + requirements: PresentationSubmissionRequirement[] + + /** + * Name of the presentation definition + */ + name?: string + + /** + * Purpose of the presentation definition. + */ + purpose?: string +} diff --git a/packages/openid4vc-client/src/presentations/transform.ts b/packages/openid4vc-client/src/presentations/transform.ts new file mode 100644 index 0000000000..4576361d49 --- /dev/null +++ b/packages/openid4vc-client/src/presentations/transform.ts @@ -0,0 +1,65 @@ +import type { W3cVerifiableCredential, W3cVerifiablePresentation } from '@aries-framework/core' +import type { + W3CVerifiableCredential as SphereonW3cVerifiableCredential, + W3CVerifiablePresentation as SphereonW3cVerifiablePresentation, +} from '@sphereon/ssi-types' + +import { + ClaimFormat, + JsonTransformer, + AriesFrameworkError, + W3cJsonLdVerifiablePresentation, + W3cJwtVerifiablePresentation, + W3cJwtVerifiableCredential, + W3cJsonLdVerifiableCredential, +} from '@aries-framework/core' + +export type { SphereonW3cVerifiableCredential, SphereonW3cVerifiablePresentation } + +export function getSphereonW3cVerifiableCredential( + w3cVerifiableCredential: W3cVerifiableCredential +): SphereonW3cVerifiableCredential { + if (w3cVerifiableCredential.claimFormat === ClaimFormat.LdpVc) { + return JsonTransformer.toJSON(w3cVerifiableCredential) as SphereonW3cVerifiableCredential + } else if (w3cVerifiableCredential.claimFormat === ClaimFormat.JwtVc) { + return w3cVerifiableCredential.serializedJwt + } else { + throw new AriesFrameworkError( + `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` + ) + } +} + +export function getSphereonW3cVerifiablePresentation( + w3cVerifiablePresentation: W3cVerifiablePresentation +): SphereonW3cVerifiablePresentation { + if (w3cVerifiablePresentation instanceof W3cJsonLdVerifiablePresentation) { + return JsonTransformer.toJSON(w3cVerifiablePresentation) as SphereonW3cVerifiablePresentation + } else if (w3cVerifiablePresentation instanceof W3cJwtVerifiablePresentation) { + return w3cVerifiablePresentation.serializedJwt + } else { + throw new AriesFrameworkError( + `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` + ) + } +} + +export function getW3cVerifiablePresentationInstance( + w3cVerifiablePresentation: SphereonW3cVerifiablePresentation +): W3cVerifiablePresentation { + if (typeof w3cVerifiablePresentation === 'string') { + return W3cJwtVerifiablePresentation.fromSerializedJwt(w3cVerifiablePresentation) + } else { + return JsonTransformer.fromJSON(w3cVerifiablePresentation, W3cJsonLdVerifiablePresentation) + } +} + +export function getW3cVerifiableCredentialInstance( + w3cVerifiableCredential: SphereonW3cVerifiableCredential +): W3cVerifiableCredential { + if (typeof w3cVerifiableCredential === 'string') { + return W3cJwtVerifiableCredential.fromSerializedJwt(w3cVerifiableCredential) + } else { + return JsonTransformer.fromJSON(w3cVerifiableCredential, W3cJsonLdVerifiableCredential) + } +} diff --git a/packages/openid4vc-client/src/utils/Formats.ts b/packages/openid4vc-client/src/utils/Formats.ts new file mode 100644 index 0000000000..7d13fa6ef9 --- /dev/null +++ b/packages/openid4vc-client/src/utils/Formats.ts @@ -0,0 +1,41 @@ +import type { OID4VCICredentialFormat } from '@sphereon/oid4vci-common' +import type { CredentialFormat } from '@sphereon/ssi-types' + +import { OpenId4VCIVersion } from '@sphereon/oid4vci-common' + +// Base on https://github.com/Sphereon-Opensource/OID4VCI/pull/54/files + +const isUniformFormat = (format: string): format is OID4VCICredentialFormat => { + return ['jwt_vc_json', 'jwt_vc_json-ld', 'ldp_vc'].includes(format) +} + +export function getUniformFormat(format: string | OID4VCICredentialFormat | CredentialFormat): OID4VCICredentialFormat { + // Already valid format + if (isUniformFormat(format)) { + return format + } + + // Older formats + if (format === 'jwt_vc' || format === 'jwt') { + return 'jwt_vc_json' + } + if (format === 'ldp_vc' || format === 'ldp') { + return 'ldp_vc' + } + + throw new Error(`Invalid format: ${format}`) +} + +export function getFormatForVersion(format: string, version: OpenId4VCIVersion) { + const uniformFormat = isUniformFormat(format) ? format : getUniformFormat(format) + + if (version < OpenId4VCIVersion.VER_1_0_11) { + if (uniformFormat === 'jwt_vc_json') { + return 'jwt_vc' as const + } else if (uniformFormat === 'ldp_vc' || uniformFormat === 'jwt_vc_json-ld') { + return 'ldp_vc' as const + } + } + + return uniformFormat +} diff --git a/packages/openid4vc-client/src/utils/IssuerMetadataUtils.ts b/packages/openid4vc-client/src/utils/IssuerMetadataUtils.ts new file mode 100644 index 0000000000..827cfdaa5e --- /dev/null +++ b/packages/openid4vc-client/src/utils/IssuerMetadataUtils.ts @@ -0,0 +1,113 @@ +import type { + CredentialIssuerMetadata, + CredentialSupported, + CredentialSupportedTypeV1_0_08, + CredentialSupportedV1_0_08, + IssuerMetadataV1_0_08, + MetadataDisplay, +} from '@sphereon/oid4vci-common' + +import { OpenId4VCIVersion } from '@sphereon/oid4vci-common' + +export function getSupportedCredentials(opts?: { + issuerMetadata?: CredentialIssuerMetadata | IssuerMetadataV1_0_08 + version: OpenId4VCIVersion + credentialSupportedIds?: string[] +}): CredentialSupported[] { + const { issuerMetadata } = opts ?? {} + let credentialsSupported: CredentialSupported[] + if (!issuerMetadata) { + return [] + } + const { version, credentialSupportedIds } = opts ?? { version: OpenId4VCIVersion.VER_1_0_11 } + + const usesTransformedCredentialsSupported = + version === OpenId4VCIVersion.VER_1_0_08 || !Array.isArray(issuerMetadata.credentials_supported) + if (usesTransformedCredentialsSupported) { + credentialsSupported = credentialsSupportedV8ToV11((issuerMetadata as IssuerMetadataV1_0_08).credentials_supported) + } else { + credentialsSupported = (issuerMetadata as CredentialIssuerMetadata).credentials_supported + } + + if (credentialsSupported === undefined || credentialsSupported.length === 0) { + return [] + } else if (!credentialSupportedIds || credentialSupportedIds.length === 0) { + return credentialsSupported + } + + const credentialSupportedOverlap: CredentialSupported[] = [] + for (const credentialSupportedId of credentialSupportedIds) { + if (typeof credentialSupportedId === 'string') { + const supported = credentialsSupported.find((sup) => { + // Match id to offerType + if (sup.id === credentialSupportedId) return true + + // If the credential was transformed and the v8 variant supported multiple formats for the id, we + // check if there is an id with the format + // see credentialsSupportedV8ToV11 + if (usesTransformedCredentialsSupported && sup.id === `${credentialSupportedId}-${sup.format}`) return true + + return false + }) + if (supported) { + credentialSupportedOverlap.push(supported) + } + } + } + + return credentialSupportedOverlap +} + +export function credentialsSupportedV8ToV11(supportedV8: CredentialSupportedTypeV1_0_08): CredentialSupported[] { + return Object.entries(supportedV8).flatMap((entry) => { + const type = entry[0] + const supportedV8 = entry[1] + return credentialSupportedV8ToV11(type, supportedV8) + }) +} + +export function credentialSupportedV8ToV11( + key: string, + supportedV8: CredentialSupportedV1_0_08 +): CredentialSupported[] { + const v8FormatEntries = Object.entries(supportedV8.formats) + + return v8FormatEntries.map((entry) => { + const format = entry[0] + const credentialSupportBrief = entry[1] + if (typeof format !== 'string') { + throw Error(`Unknown format received ${JSON.stringify(format)}`) + } + let credentialSupport: Partial = {} + + // v8 format included the credential type / id as the key of the object and it could contain multiple supported formats + // v11 format has an array where each entry only supports one format, and can only have an `id` property. We include the + // key from the v8 object as the id for the v11 object, but to prevent collisions (as multiple formats can be supported under + // one key), we append the format to the key IF there's more than one format supported under the key. + const id = v8FormatEntries.length > 1 ? `${key}-${format}` : key + + credentialSupport = { + format, + display: supportedV8.display, + ...credentialSupportBrief, + credentialSubject: supportedV8.claims, + id, + } + return credentialSupport as CredentialSupported + }) +} + +export function getIssuerDisplays( + metadata: CredentialIssuerMetadata | IssuerMetadataV1_0_08, + opts?: { prefLocales: string[] } +): MetadataDisplay[] { + const matchedDisplays = + metadata.display?.filter( + (item) => + !opts?.prefLocales || + opts.prefLocales.length === 0 || + (item.locale && opts.prefLocales.includes(item.locale)) || + !item.locale + ) ?? [] + return matchedDisplays.sort((item) => (item.locale ? opts?.prefLocales.indexOf(item.locale) ?? 1 : Number.MAX_VALUE)) +} diff --git a/packages/openid4vc-client/src/utils/__tests__/claimFormatMapping.test.ts b/packages/openid4vc-client/src/utils/__tests__/claimFormatMapping.test.ts new file mode 100644 index 0000000000..a8bcdd9633 --- /dev/null +++ b/packages/openid4vc-client/src/utils/__tests__/claimFormatMapping.test.ts @@ -0,0 +1,45 @@ +import { AriesFrameworkError, ClaimFormat } from '@aries-framework/core' + +import { + fromDifClaimFormatToOpenIdCredentialFormatProfile, + fromOpenIdCredentialFormatProfileToDifClaimFormat, + OpenIdCredentialFormatProfile, +} from '../claimFormatMapping' + +describe('claimFormatMapping', () => { + it('should convert from openid credential format profile to DIF claim format', () => { + expect(fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.LdpVc)).toStrictEqual( + OpenIdCredentialFormatProfile.LdpVc + ) + + expect(fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.JwtVc)).toStrictEqual( + OpenIdCredentialFormatProfile.JwtVcJson + ) + + expect(() => fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.Jwt)).toThrow(AriesFrameworkError) + + expect(() => fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.Ldp)).toThrow(AriesFrameworkError) + + expect(() => fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.JwtVp)).toThrow(AriesFrameworkError) + + expect(() => fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.LdpVp)).toThrow(AriesFrameworkError) + }) + + it('should convert from DIF claim format to openid credential format profile', () => { + expect(fromOpenIdCredentialFormatProfileToDifClaimFormat(OpenIdCredentialFormatProfile.JwtVcJson)).toStrictEqual( + ClaimFormat.JwtVc + ) + + expect(fromOpenIdCredentialFormatProfileToDifClaimFormat(OpenIdCredentialFormatProfile.JwtVcJsonLd)).toStrictEqual( + ClaimFormat.JwtVc + ) + + expect(fromOpenIdCredentialFormatProfileToDifClaimFormat(OpenIdCredentialFormatProfile.LdpVc)).toStrictEqual( + ClaimFormat.LdpVc + ) + + expect(() => fromOpenIdCredentialFormatProfileToDifClaimFormat(OpenIdCredentialFormatProfile.MsoMdoc)).toThrow( + AriesFrameworkError + ) + }) +}) diff --git a/packages/openid4vc-client/src/utils/claimFormatMapping.ts b/packages/openid4vc-client/src/utils/claimFormatMapping.ts new file mode 100644 index 0000000000..3ab952f94f --- /dev/null +++ b/packages/openid4vc-client/src/utils/claimFormatMapping.ts @@ -0,0 +1,40 @@ +import { AriesFrameworkError, ClaimFormat } from '@aries-framework/core' + +export enum OpenIdCredentialFormatProfile { + JwtVcJson = 'jwt_vc_json', + JwtVcJsonLd = 'jwt_vc_json-ld', + LdpVc = 'ldp_vc', + MsoMdoc = 'mso_mdoc', +} + +export const fromDifClaimFormatToOpenIdCredentialFormatProfile = ( + claimFormat: ClaimFormat +): OpenIdCredentialFormatProfile => { + switch (claimFormat) { + case ClaimFormat.JwtVc: + return OpenIdCredentialFormatProfile.JwtVcJson + case ClaimFormat.LdpVc: + return OpenIdCredentialFormatProfile.LdpVc + default: + throw new AriesFrameworkError( + `Unsupported DIF claim format, ${claimFormat}, to map to an openid credential format profile` + ) + } +} + +export const fromOpenIdCredentialFormatProfileToDifClaimFormat = ( + openidCredentialFormatProfile: OpenIdCredentialFormatProfile +): ClaimFormat => { + switch (openidCredentialFormatProfile) { + case OpenIdCredentialFormatProfile.JwtVcJson: + return ClaimFormat.JwtVc + case OpenIdCredentialFormatProfile.JwtVcJsonLd: + return ClaimFormat.JwtVc + case OpenIdCredentialFormatProfile.LdpVc: + return ClaimFormat.LdpVc + default: + throw new AriesFrameworkError( + `Unsupported openid credential format profile, ${openidCredentialFormatProfile}, to map to a DIF claim format` + ) + } +} diff --git a/packages/openid4vc-client/src/utils/index.ts b/packages/openid4vc-client/src/utils/index.ts new file mode 100644 index 0000000000..ee55f476ed --- /dev/null +++ b/packages/openid4vc-client/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './claimFormatMapping' +export * from './metadata' diff --git a/packages/openid4vc-client/src/utils/metadata.ts b/packages/openid4vc-client/src/utils/metadata.ts new file mode 100644 index 0000000000..fd659be10b --- /dev/null +++ b/packages/openid4vc-client/src/utils/metadata.ts @@ -0,0 +1,70 @@ +import type { W3cCredentialRecord } from '@aries-framework/core' +import type { + CredentialIssuerMetadata, + CredentialsSupportedDisplay, + CredentialSupported, + EndpointMetadata, + EndpointMetadataResult, + IssuerCredentialSubject, + IssuerMetadataV1_0_08, + MetadataDisplay, +} from '@sphereon/oid4vci-common' + +export interface OpenId4VcCredentialMetadata { + credential: { + display?: CredentialsSupportedDisplay[] + order?: string[] + credentialSubject: IssuerCredentialSubject + } + issuer: { + display?: MetadataDisplay[] + id: string + } +} + +// what does this mean +const openId4VcCredentialMetadataKey = '_paradym/openId4VcCredentialMetadata' + +function extractOpenId4VcCredentialMetadata( + credentialMetadata: CredentialSupported, + serverMetadata: EndpointMetadata, + serverMetadataResult: CredentialIssuerMetadata | IssuerMetadataV1_0_08 +) { + return { + credential: { + display: credentialMetadata.display, + order: credentialMetadata.order, + credentialSubject: credentialMetadata.credentialSubject, + }, + issuer: { + display: serverMetadataResult.credentialIssuerMetadata?.display, + id: serverMetadata.issuer, + }, + } +} + +/** + * Gets the OpenId4Vc credential metadata from the given W3C credential record. + */ +export function getOpenId4VcCredentialMetadata( + w3cCredentialRecord: W3cCredentialRecord +): OpenId4VcCredentialMetadata | null { + return w3cCredentialRecord.metadata.get(openId4VcCredentialMetadataKey) +} + +/** + * Sets the OpenId4Vc credential metadata on the given W3C credential record. + * + * NOTE: this does not save the record. + */ +export function setOpenId4VcCredentialMetadata( + w3cCredentialRecord: W3cCredentialRecord, + credentialMetadata: CredentialSupported, + serverMetadata: EndpointMetadata, + serverMetadataResult: CredentialIssuerMetadata | IssuerMetadataV1_0_08 +) { + w3cCredentialRecord.metadata.set( + openId4VcCredentialMetadataKey, + extractOpenId4VcCredentialMetadata(credentialMetadata, serverMetadata, serverMetadataResult) + ) +} diff --git a/packages/openid4vc-client/src/__tests__/OpenId4VcClientModule.test.ts b/packages/openid4vc-client/tests/OpenId4VcClientModule.test.ts similarity index 61% rename from packages/openid4vc-client/src/__tests__/OpenId4VcClientModule.test.ts rename to packages/openid4vc-client/tests/OpenId4VcClientModule.test.ts index be2ad9fd39..b3383d8d6c 100644 --- a/packages/openid4vc-client/src/__tests__/OpenId4VcClientModule.test.ts +++ b/packages/openid4vc-client/tests/OpenId4VcClientModule.test.ts @@ -1,8 +1,10 @@ +/* eslint-disable @typescript-eslint/unbound-method */ import type { DependencyManager } from '@aries-framework/core' -import { OpenId4VcClientApi } from '../OpenId4VcClientApi' -import { OpenId4VcClientModule } from '../OpenId4VcClientModule' -import { OpenId4VcClientService } from '../OpenId4VcClientService' +import { OpenId4VcClientApi } from '../src/OpenId4VcClientApi' +import { OpenId4VcClientModule } from '../src/OpenId4VcClientModule' +import { OpenId4VcClientService } from '../src/OpenId4VcClientService' +import { OpenId4VpClientService, PresentationExchangeService } from '../src/presentations' const dependencyManager = { registerInstance: jest.fn(), @@ -19,7 +21,9 @@ describe('OpenId4VcClientModule', () => { expect(dependencyManager.registerContextScoped).toHaveBeenCalledTimes(1) expect(dependencyManager.registerContextScoped).toHaveBeenCalledWith(OpenId4VcClientApi) - expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(3) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VcClientService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VpClientService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(PresentationExchangeService) }) }) diff --git a/packages/openid4vc-client/tests/fixtures.ts b/packages/openid4vc-client/tests/fixtures.ts index b8b322a428..54e46bb496 100644 --- a/packages/openid4vc-client/tests/fixtures.ts +++ b/packages/openid4vc-client/tests/fixtures.ts @@ -1,4 +1,4 @@ -export const mattrLaunchpadJsonLd = { +export const mattrLaunchpadJsonLd_draft_08 = { credentialOffer: 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=OpenBadgeCredential&pre-authorized_code=krBcsBIlye2T-G4-rHHnRZUCah9uzDKwohJK6ABNvL-', getMetadataResponse: { @@ -136,7 +136,7 @@ export const mattrLaunchpadJsonLd = { }, } -export const waltIdJffJwt = { +export const waltIdJffJwt_draft_08 = { credentialOffer: 'openid-initiate-issuance://?issuer=https%3A%2F%2Fjff.walt.id%2Fissuer-api%2Fdefault%2Foidc%2F&credential_type=VerifiableId&pre-authorized_code=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI4YmI0NWZiNC0zNDc1LTQ5YzItODVjNy0wYjkxZjY4N2RhNDQiLCJwcmUtYXV0aG9yaXplZCI6dHJ1ZX0.R8nHseZJvU3uVL3Ox-97i1HUnvjZH6wKSWDO_i8D12I&user_pin_required=false', getMetadataResponse: { @@ -303,9 +303,143 @@ export const waltIdJffJwt = { }, }, }, - credential_issuer: { display: [{ locale: null, name: 'https://jff.walt.id/issuer-api/default' }] }, + credential_issuer: { + display: [{ locale: null, name: 'https://jff.walt.id/issuer-api/default' }], + }, + credential_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/credential', + subject_types_supported: ['public'], + }, + + acquireAccessTokenResponse: { + access_token: '8bb45fb4-3475-49c2-85c7-0b91f687da44', + refresh_token: 'WEjORX8NZccRGtRN4yvXFdYE8MeAOaLLmmGlcRbutq4', + c_nonce: 'cbad6376-f882-44c5-ae88-19bccc0de124', + id_token: + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI4YmI0NWZiNC0zNDc1LTQ5YzItODVjNy0wYjkxZjY4N2RhNDQifQ.Mca0Ln1AvNlxBJftYc1PZKQBlGdBmrHsFRQSBDoCgD0', + token_type: 'Bearer', + expires_in: 300, + }, + + credentialResponse: { + credential: + 'eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCMwIiwidHlwIjoiSldUIiwiYWxnIjoiRWREU0EifQ.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCIsInN1YiI6ImRpZDprZXk6ekRuYWVpcFdnOURNWFB0OWpjbUFCcWFZUlZLYzE5dFgxeGZCUldGc0pTUG9VZE1udiIsIm5iZiI6MTY4NTM1MDc4OSwiaWF0IjoxNjg1MzUwNzg5LCJ2YyI6eyJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVmVyaWZpYWJsZUF0dGVzdGF0aW9uIiwiVmVyaWZpYWJsZUlkIl0sIkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sImlkIjoidXJuOnV1aWQ6NTljZTRhYzItZWM2NS00YjhmLThmOTYtZWE3ODUxMmRmOWQzIiwiaXNzdWVyIjoiZGlkOmp3azpleUpyZEhraU9pSlBTMUFpTENKMWMyVWlPaUp6YVdjaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWEybGtJam9pTjJRMlkySm1NalE0T1dJek5ESTNObUl4TnpJeE9UQTFORGxrTWpNNU1UZ2lMQ0o0SWpvaVJtNUZWVlZoZFdSdE9UbE9NekJpT0RCcWN6aFdkRFJCYms5NGRsSjNXSFJuVW1OTGNUTm5Ra2wxT0NJc0ltRnNaeUk2SWtWa1JGTkJJbjAiLCJpc3N1YW5jZURhdGUiOiIyMDIzLTA1LTI5VDA4OjU5OjQ5WiIsImlzc3VlZCI6IjIwMjMtMDUtMjlUMDg6NTk6NDlaIiwidmFsaWRGcm9tIjoiMjAyMy0wNS0yOVQwODo1OTo0OVoiLCJjcmVkZW50aWFsU2NoZW1hIjp7ImlkIjoiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3dhbHQtaWQvd2FsdGlkLXNzaWtpdC12Y2xpYi9tYXN0ZXIvc3JjL3Rlc3QvcmVzb3VyY2VzL3NjaGVtYXMvVmVyaWZpYWJsZUlkLmpzb24iLCJ0eXBlIjoiRnVsbEpzb25TY2hlbWFWYWxpZGF0b3IyMDIxIn0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6RG5hZWlwV2c5RE1YUHQ5amNtQUJxYVlSVktjMTl0WDF4ZkJSV0ZzSlNQb1VkTW52IiwiY3VycmVudEFkZHJlc3MiOlsiMSBCb3VsZXZhcmQgZGUgbGEgTGliZXJ0w6ksIDU5ODAwIExpbGxlIl0sImRhdGVPZkJpcnRoIjoiMTk5My0wNC0wOCIsImZhbWlseU5hbWUiOiJET0UiLCJmaXJzdE5hbWUiOiJKYW5lIiwiZ2VuZGVyIjoiRkVNQUxFIiwibmFtZUFuZEZhbWlseU5hbWVBdEJpcnRoIjoiSmFuZSBET0UiLCJwZXJzb25hbElkZW50aWZpZXIiOiIwOTA0MDA4MDg0SCIsInBsYWNlT2ZCaXJ0aCI6IkxJTExFLCBGUkFOQ0UifSwiZXZpZGVuY2UiOlt7ImRvY3VtZW50UHJlc2VuY2UiOlsiUGh5c2ljYWwiXSwiZXZpZGVuY2VEb2N1bWVudCI6WyJQYXNzcG9ydCJdLCJzdWJqZWN0UHJlc2VuY2UiOiJQaHlzaWNhbCIsInR5cGUiOlsiRG9jdW1lbnRWZXJpZmljYXRpb24iXSwidmVyaWZpZXIiOiJkaWQ6ZWJzaToyQTlCWjlTVWU2QmF0YWNTcHZzMVY1Q2RqSHZMcFE3YkVzaTJKYjZMZEhLblF4YU4ifV19LCJqdGkiOiJ1cm46dXVpZDo1OWNlNGFjMi1lYzY1LTRiOGYtOGY5Ni1lYTc4NTEyZGY5ZDMifQ.6Wn8X2tEQJ9CmX3-meCxDuGmevRdtivnjVkGPXzfnJ-1M6AU4SFxxon0JmMjdmO_h4P9sCEe9RTtyTJou2yeCA', + format: 'jwt_vc', + }, +} + +// This object is MANUALLY converted and should be updated when we have actual test vectors +export const waltIdJffJwt_draft_11 = { + credentialOffer: + 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fjff.walt.id%2Fissuer-api%2Fdefault%2Foidc%22%2C%22credentials%22%3A%5B%22VerifiableId%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22ABC%22%7D%7D%7D', + getMetadataResponse: { + authorization_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/fulfillPAR', + token_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/token', + pushed_authorization_request_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/par', + credential_issuer: 'https://jff.walt.id/issuer-api/default', + jwks_uri: 'https://jff.walt.id/issuer-api/default/oidc', credential_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/credential', subject_types_supported: ['public'], + grant_types_supported: ['authorization_code', 'urn:ietf:params:oauth:grant-type:pre-authorized_code'], + request_uri_parameter_supported: true, + credentials_supported: [ + { + id: 'VerifiableId', + format: 'jwt_vc_json', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], + types: ['VerifiableCredential', 'OpenBadgeCredential'], + }, + { + id: 'VerifiableDiploma', + display: [{ name: 'VerifiableDiploma' }], + format: 'ldp_vc', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: [ + 'Ed25519Signature2018', + 'Ed25519Signature2020', + 'EcdsaSecp256k1Signature2019', + 'RsaSignature2018', + 'JsonWebSignature2020', + 'JcsEd25519Signature2020', + ], + types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableDiploma'], + }, + { + id: 'VerifiableVaccinationCertificate', + display: [{ name: 'VerifiableVaccinationCertificate' }], + format: 'ldp_vc', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: [ + 'Ed25519Signature2018', + 'Ed25519Signature2020', + 'EcdsaSecp256k1Signature2019', + 'RsaSignature2018', + 'JsonWebSignature2020', + 'JcsEd25519Signature2020', + ], + types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableVaccinationCertificate'], + }, + { + id: 'ProofOfResidence', + display: [{ name: 'ProofOfResidence' }], + format: 'ldp_vc', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: [ + 'Ed25519Signature2018', + 'Ed25519Signature2020', + 'EcdsaSecp256k1Signature2019', + 'RsaSignature2018', + 'JsonWebSignature2020', + 'JcsEd25519Signature2020', + ], + types: ['VerifiableCredential', 'VerifiableAttestation', 'ProofOfResidence'], + }, + { + id: 'ParticipantCredential', + format: 'ldp_vc', + display: [{ name: 'ParticipantCredential' }], + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: [ + 'Ed25519Signature2018', + 'Ed25519Signature2020', + 'EcdsaSecp256k1Signature2019', + 'RsaSignature2018', + 'JsonWebSignature2020', + 'JcsEd25519Signature2020', + ], + types: ['VerifiableCredential', 'ParticipantCredential'], + }, + { + id: 'Europass', + display: [{ name: 'Europass' }], + format: 'ldp_vc', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: [ + 'Ed25519Signature2018', + 'Ed25519Signature2020', + 'EcdsaSecp256k1Signature2019', + 'RsaSignature2018', + 'JsonWebSignature2020', + 'JcsEd25519Signature2020', + ], + types: ['VerifiableCredential', 'VerifiableAttestation', 'Europass'], + }, + { + id: 'OpenBadgeCredential', + display: [{ name: 'OpenBadgeCredential' }], + format: 'ldp_vc', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: [ + 'Ed25519Signature2018', + 'Ed25519Signature2020', + 'EcdsaSecp256k1Signature2019', + 'RsaSignature2018', + 'JsonWebSignature2020', + 'JcsEd25519Signature2020', + ], + types: ['VerifiableCredential', 'OpenBadgeCredential'], + }, + ], }, acquireAccessTokenResponse: { diff --git a/packages/openid4vc-client/tests/openid4vc-client.e2e.test.ts b/packages/openid4vc-client/tests/openid4vc-client.e2e.test.ts index 556f1c07b4..55fe329317 100644 --- a/packages/openid4vc-client/tests/openid4vc-client.e2e.test.ts +++ b/packages/openid4vc-client/tests/openid4vc-client.e2e.test.ts @@ -1,41 +1,51 @@ import type { KeyDidCreateOptions } from '@aries-framework/core' +import { AskarModule } from '@aries-framework/askar' import { - ClaimFormat, JwaSignatureAlgorithm, Agent, KeyType, TypedArrayEncoder, W3cCredentialRecord, - W3cCredentialsModule, DidKey, } from '@aries-framework/core' +import { agentDependencies } from '@aries-framework/node' +import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import nock, { cleanAll, enableNetConnect } from 'nock' -import { AskarModule } from '../../askar/src' -import { askarModuleConfig } from '../../askar/tests/helpers' -import { customDocumentLoader } from '../../core/src/modules/vc/data-integrity/__tests__/documentLoader' -import { getAgentOptions } from '../../core/tests' +import { OpenId4VcClientModule } from '../src' +import { OpenIdCredentialFormatProfile } from '../src/utils/claimFormatMapping' -import { mattrLaunchpadJsonLd, waltIdJffJwt } from './fixtures' - -import { OpenId4VcClientModule } from '@aries-framework/openid4vc-client' +import { + mattrLaunchpadJsonLd_draft_08, + // FIXME: we need a custom document loader for this, which is only present in AFJ core + // mattrLaunchpadJsonLd_draft_08, + waltIdJffJwt_draft_08, + waltIdJffJwt_draft_11, +} from './fixtures' const modules = { openId4VcClient: new OpenId4VcClientModule(), - w3cCredentials: new W3cCredentialsModule({ - documentLoader: customDocumentLoader, + askar: new AskarModule({ + ariesAskar, }), - askar: new AskarModule(askarModuleConfig), } describe('OpenId4VcClient', () => { let agent: Agent beforeEach(async () => { - const agentOptions = getAgentOptions('OpenId4VcClient Agent', {}, modules) - - agent = new Agent(agentOptions) + agent = new Agent({ + config: { + label: 'OpenId4VcClient Test', + walletConfig: { + id: 'openid4vc-client-test', + key: 'openid4vc-client-test', + }, + }, + dependencies: agentDependencies, + modules, + }) await agent.initialize() }) @@ -45,33 +55,44 @@ describe('OpenId4VcClient', () => { await agent.wallet.delete() }) - describe('Pre-authorized flow', () => { + describe('[DRAFT 08]: Pre-authorized flow', () => { afterEach(() => { cleanAll() enableNetConnect() }) - it('Should successfully execute the pre-authorized flow using a did:key Ed25519 subject and JSON-LD credential', async () => { + xit('[DRAFT 08]: Should successfully execute the pre-authorized flow using a did:key Ed25519 subject and JSON-LD credential', async () => { + const fixture = mattrLaunchpadJsonLd_draft_08 /** * Below we're setting up some mock HTTP responses. * These responses are based on the openid-initiate-issuance URI above * */ // setup temporary redirect mock - nock('https://launchpad.mattrlabs.com').get('/.well-known/openid-credential-issuer').reply(307, undefined, { - Location: 'https://launchpad.vii.electron.mattrlabs.io/.well-known/openid-credential-issuer', - }) + nock('https://launchpad.mattrlabs.com') + .get('/.well-known/openid-credential-issuer') + .reply(307, undefined, { + Location: 'https://launchpad.vii.electron.mattrlabs.io/.well-known/openid-credential-issuer', + }) + + .get('/.well-known/openid-configuration') + .reply(404) + + .get('/.well-known/oauth-authorization-server') + .reply(404) // setup server metadata response - const httpMock = nock('https://launchpad.vii.electron.mattrlabs.io') + nock('https://launchpad.vii.electron.mattrlabs.io') .get('/.well-known/openid-credential-issuer') - .reply(200, mattrLaunchpadJsonLd.getMetadataResponse) + .reply(200, fixture.getMetadataResponse) - // setup access token response - httpMock.post('/oidc/v1/auth/token').reply(200, mattrLaunchpadJsonLd.acquireAccessTokenResponse) + // setup access token response + .post('/oidc/v1/auth/token') + .reply(200, fixture.acquireAccessTokenResponse) - // setup credential request response - httpMock.post('/oidc/v1/auth/credential').reply(200, mattrLaunchpadJsonLd.credentialResponse) + // setup credential request response + .post('/oidc/v1/auth/credential') + .reply(200, fixture.credentialResponse) const did = await agent.dids.create({ method: 'key', @@ -89,7 +110,7 @@ describe('OpenId4VcClient', () => { if (!verificationMethod) throw new Error('No verification method found') const w3cCredentialRecords = await agent.modules.openId4VcClient.requestCredentialUsingPreAuthorizedCode({ - issuerUri: mattrLaunchpadJsonLd.credentialOffer, + issuerUri: fixture.credentialOffer, verifyCredentialStatus: false, // We only allow EdDSa, as we've created a did with keyType ed25519. If we create // or determine the did dynamically we could use any signature algorithm @@ -98,7 +119,7 @@ describe('OpenId4VcClient', () => { }) expect(w3cCredentialRecords).toHaveLength(1) - const w3cCredentialRecord = w3cCredentialRecords[0] + const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord expect(w3cCredentialRecord).toBeInstanceOf(W3cCredentialRecord) expect(w3cCredentialRecord.credential.type).toEqual([ @@ -110,19 +131,25 @@ describe('OpenId4VcClient', () => { expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) }) - it('Should successfully execute the pre-authorized flow using a did:key P256 subject and JWT credential', async () => { - /** - * Below we're setting up some mock HTTP responses. - * These responses are based on the openid-initiate-issuance URI above - */ - // setup server metadata response - const httpMock = nock('https://jff.walt.id/issuer-api/default/oidc') + it('[DRAFT 08]: Should successfully execute the pre-authorized flow using a did:key P256 subject and JWT credential', async () => { + const fixture = waltIdJffJwt_draft_08 + + nock('https://jff.walt.id/issuer-api/default/oidc') + // metadata .get('/.well-known/openid-credential-issuer') - .reply(200, waltIdJffJwt.getMetadataResponse) - // setup access token response - httpMock.post('/token').reply(200, waltIdJffJwt.credentialResponse) - // setup credential request response - httpMock.post('/credential').reply(200, waltIdJffJwt.credentialResponse) + .reply(200, fixture.getMetadataResponse) + .get('/.well-known/openid-configuration') + .reply(404) + .get('/.well-known/oauth-authorization-server') + .reply(404) + + // setup access token response + .post('/token') + .reply(200, fixture.credentialResponse) + + // setup credential request response + .post('/credential') + .reply(200, fixture.credentialResponse) const did = await agent.dids.create({ method: 'key', @@ -140,15 +167,15 @@ describe('OpenId4VcClient', () => { if (!verificationMethod) throw new Error('No verification method found') const w3cCredentialRecords = await agent.modules.openId4VcClient.requestCredentialUsingPreAuthorizedCode({ - issuerUri: waltIdJffJwt.credentialOffer, - allowedCredentialFormats: [ClaimFormat.JwtVc], + issuerUri: fixture.credentialOffer, + allowedCredentialFormats: [OpenIdCredentialFormatProfile.JwtVcJson], allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], proofOfPossessionVerificationMethodResolver: () => verificationMethod, verifyCredentialStatus: false, }) expect(w3cCredentialRecords[0]).toBeInstanceOf(W3cCredentialRecord) - const w3cCredentialRecord = w3cCredentialRecords[0] + const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord expect(w3cCredentialRecord.credential.type).toEqual([ 'VerifiableCredential', @@ -160,36 +187,39 @@ describe('OpenId4VcClient', () => { }) }) - describe('Authorization flow', () => { - beforeAll(async () => { - /** - * Below we're setting up some mock HTTP responses. - * These responses are based on the openid-initiate-issuance URI above - * */ + describe('[DRAFT 08]: Authorization flow', () => { + afterAll(() => { + cleanAll() + enableNetConnect() + }) + + it('[DRAFT 08]: should generate a valid authorization url', async () => { + const fixture = mattrLaunchpadJsonLd_draft_08 // setup temporary redirect mock - nock('https://launchpad.mattrlabs.com').get('/.well-known/openid-credential-issuer').reply(307, undefined, { - Location: 'https://launchpad.vii.electron.mattrlabs.io/.well-known/openid-credential-issuer', - }) + nock('https://launchpad.mattrlabs.com') + .get('/.well-known/openid-credential-issuer') + .reply(307, undefined, { + Location: 'https://launchpad.vii.electron.mattrlabs.io/.well-known/openid-credential-issuer', + }) + .get('/.well-known/openid-configuration') + .reply(404) + .get('/.well-known/oauth-authorization-server') + .reply(404) // setup server metadata response - const httpMock = nock('https://launchpad.vii.electron.mattrlabs.io') + nock('https://launchpad.vii.electron.mattrlabs.io') .get('/.well-known/openid-credential-issuer') - .reply(200, mattrLaunchpadJsonLd.getMetadataResponse) + .reply(200, fixture.getMetadataResponse) - // setup access token response - httpMock.post('/oidc/v1/auth/token').reply(200, mattrLaunchpadJsonLd.acquireAccessTokenResponse) + // setup access token response + .post('/oidc/v1/auth/token') + .reply(200, fixture.acquireAccessTokenResponse) - // setup credential request response - httpMock.post('/oidc/v1/auth/credential').reply(200, mattrLaunchpadJsonLd.credentialResponse) - }) + // setup credential request response + .post('/oidc/v1/auth/credential') + .reply(200, fixture.credentialResponse) - afterAll(async () => { - cleanAll() - enableNetConnect() - }) - - it('should generate a valid authorization url', async () => { const clientId = 'test-client' const redirectUri = 'https://example.com/cb' @@ -212,7 +242,10 @@ describe('OpenId4VcClient', () => { expect(parsedUrl.searchParams.get('code_challenge_method')).toBe('S256') expect(parsedUrl.searchParams.get('redirect_uri')).toBe(redirectUri) }) - it('should throw if no scope is provided', async () => { + + it('[DRAFT 08]: should throw if no scope is provided', async () => { + const fixture = mattrLaunchpadJsonLd_draft_08 + // setup temporary redirect mock nock('https://launchpad.mattrlabs.com').get('/.well-known/openid-credential-issuer').reply(307, undefined, { Location: 'https://launchpad.vii.electron.mattrlabs.io/.well-known/openid-credential-issuer', @@ -221,13 +254,30 @@ describe('OpenId4VcClient', () => { // setup server metadata response nock('https://launchpad.vii.electron.mattrlabs.io') .get('/.well-known/openid-credential-issuer') - .reply(200, mattrLaunchpadJsonLd.getMetadataResponse) + .reply(200, fixture.getMetadataResponse) + .get('/.well-known/openid-configuration') + .reply(404) + .get('/.well-known/oauth-authorization-server') + .reply(404) + + // setup access token response + .post('/oidc/v1/auth/token') + .reply(200, fixture.acquireAccessTokenResponse) + + // setup credential request response + .post('/oidc/v1/auth/credential') + .reply(200, fixture.credentialResponse) + + // setup server metadata response + nock('https://launchpad.vii.electron.mattrlabs.io') + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.getMetadataResponse) const clientId = 'test-client' const redirectUri = 'https://example.com/cb' const initiationUri = 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=OpenBadgeCredential' - expect( + await expect( agent.modules.openId4VcClient.generateAuthorizationUrl({ clientId, redirectUri, @@ -236,7 +286,11 @@ describe('OpenId4VcClient', () => { }) ).rejects.toThrow() }) - it('should successfully execute request a credential', async () => { + + // Need custom document loader for this + xit('[DRAFT 08]: should successfully execute request a credential', async () => { + const fixture = mattrLaunchpadJsonLd_draft_08 + // setup temporary redirect mock nock('https://launchpad.mattrlabs.com').get('/.well-known/openid-credential-issuer').reply(307, undefined, { Location: 'https://launchpad.vii.electron.mattrlabs.io/.well-known/openid-credential-issuer', @@ -245,7 +299,19 @@ describe('OpenId4VcClient', () => { // setup server metadata response nock('https://launchpad.vii.electron.mattrlabs.io') .get('/.well-known/openid-credential-issuer') - .reply(200, mattrLaunchpadJsonLd.getMetadataResponse) + .reply(200, fixture.getMetadataResponse) + .get('/.well-known/openid-configuration') + .reply(404) + .get('/.well-known/oauth-authorization-server') + .reply(404) + + // setup access token response + .post('/oidc/v1/auth/token') + .reply(200, fixture.acquireAccessTokenResponse) + + // setup credential request response + .post('/oidc/v1/auth/credential') + .reply(200, fixture.credentialResponse) const did = await agent.dids.create({ method: 'key', @@ -282,12 +348,12 @@ describe('OpenId4VcClient', () => { verifyCredentialStatus: false, proofOfPossessionVerificationMethodResolver: () => verificationMethod, allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - issuerUri: initiationUri, + issuerUri: initiationUri, // TODO redirectUri: redirectUri, }) expect(w3cCredentialRecords).toHaveLength(1) - const w3cCredentialRecord = w3cCredentialRecords[0] + const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord expect(w3cCredentialRecord).toBeInstanceOf(W3cCredentialRecord) expect(w3cCredentialRecord.credential.type).toEqual([ @@ -299,4 +365,119 @@ describe('OpenId4VcClient', () => { expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) }) }) + + describe('[DRAFT 11]: Pre-authorized flow', () => { + afterEach(() => { + cleanAll() + enableNetConnect() + }) + + // it('[DRAFT 11]: Should successfully execute the pre-authorized flow using a did:key Ed25519 subject and JSON-LD credential', async () => { + // const fixture = waltIdJffJwt_draft_11 + + // nock('https://jff.walt.id/issuer-api/default/oidc') + // .get('/.well-known/openid-credential-issuer') + // .reply(200, fixture.getMetadataResponse) + + // // setup access token response + // .post('/token') + // .reply(200, fixture.credentialResponse) + + // // setup credential request response + // .post('/credential') + // .reply(200, fixture.credentialResponse) + + // const did = await agent.dids.create({ + // method: 'key', + // options: { + // keyType: KeyType.Ed25519, + // }, + // secret: { + // privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), + // }, + // }) + + // const didKey = DidKey.fromDid(did.didState.did as string) + // const kid = `${did.didState.did as string}#${didKey.key.fingerprint}` + // const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) + // if (!verificationMethod) throw new Error('No verification method found') + + // const w3cCredentialRecords = await agent.modules.openId4VcClient.requestCredentialUsingPreAuthorizedCode({ + // uri: fixture.credentialOffer, + // verifyCredentialStatus: false, + // // We only allow EdDSa, as we've created a did with keyType ed25519. If we create + // // or determine the did dynamically we could use any signature algorithm + // allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + // proofOfPossessionVerificationMethodResolver: () => verificationMethod, + // }) + + // expect(w3cCredentialRecords).toHaveLength(1) + // const w3cCredentialRecord = w3cCredentialRecords[0] + // expect(w3cCredentialRecord).toBeInstanceOf(W3cCredentialRecord) + + // expect(w3cCredentialRecord.credential.type).toEqual([ + // 'VerifiableCredential', + // 'VerifiableAttestation', + // 'VerifiableId', + // ]) + + // expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) + // }) + + it('[DRAFT 11]: Should successfully execute the pre-authorized flow using a did:key P256 subject and JWT credential', async () => { + const fixture = waltIdJffJwt_draft_11 + + /** + * Below we're setting up some mock HTTP responses. + * These responses are based on the openid-initiate-issuance URI above + */ + // setup server metadata response + const httpMock = nock('https://jff.walt.id/issuer-api/default/oidc') + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.getMetadataResponse) + .get('/.well-known/openid-configuration') + .reply(404) + .get('/.well-known/oauth-authorization-server') + .reply(404) + + // setup access token response + httpMock.post('/token').reply(200, fixture.credentialResponse) + // setup credential request response + httpMock.post('/credential').reply(200, fixture.credentialResponse) + + const did = await agent.dids.create({ + method: 'key', + options: { + keyType: KeyType.P256, + }, + secret: { + privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), + }, + }) + + const didKey = DidKey.fromDid(did.didState.did as string) + const kid = `${didKey.did}#${didKey.key.fingerprint}` + const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) + if (!verificationMethod) throw new Error('No verification method found') + + const w3cCredentialRecords = await agent.modules.openId4VcClient.requestCredentialUsingPreAuthorizedCode({ + issuerUri: fixture.credentialOffer, + allowedCredentialFormats: [OpenIdCredentialFormatProfile.JwtVcJson], + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], + proofOfPossessionVerificationMethodResolver: () => verificationMethod, + verifyCredentialStatus: false, + }) + + expect(w3cCredentialRecords[0]).toBeInstanceOf(W3cCredentialRecord) + const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord + + expect(w3cCredentialRecord.credential.type).toEqual([ + 'VerifiableCredential', + 'VerifiableAttestation', + 'VerifiableId', + ]) + + expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) + }) + }) }) diff --git a/yarn.lock b/yarn.lock index d61b022667..e34e4e53ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,6 +10,13 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" +"@astronautlabs/jsonpath@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@astronautlabs/jsonpath/-/jsonpath-1.1.2.tgz#af19bb4a7d13dcfbc60c3c998ee1e73d7c2ddc38" + integrity sha512-FqL/muoreH7iltYC1EB5Tvox5E8NSOOPGkgns4G+qxRKl6k5dxEVljUjB5NcKESzkqwnUqWjSZkL61XGYOuV+A== + dependencies: + static-eval "2.0.2" + "@azure/core-asynciterator-polyfill@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@azure/core-asynciterator-polyfill/-/core-asynciterator-polyfill-1.0.2.tgz#0dd3849fb8d97f062a39db0e5cadc9ffaf861fec" @@ -2401,15 +2408,108 @@ resolved "https://registry.yarnpkg.com/@sovpro/delimited-stream/-/delimited-stream-1.1.0.tgz#4334bba7ee241036e580fdd99c019377630d26b4" integrity sha512-kQpk267uxB19X3X2T1mvNMjyvIEonpNSHrMlK5ZaBU6aZxw7wPbpgKJOjHN3+/GPVpXgAV9soVT2oyHpLkLtyw== -"@sphereon/openid4vci-client@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@sphereon/openid4vci-client/-/openid4vci-client-0.4.0.tgz#f48c2bb42041b9eab13669de23ba917785c83b24" - integrity sha512-N9ytyV3DHAjBjd67jMowmBMmD9/4Sxkehsrpd1I9Hxg5TO1K+puUPsPXj8Zh4heIWSzT5xBsGTSqXdF0LlrDwQ== +"@sphereon/did-auth-siop@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@sphereon/did-auth-siop/-/did-auth-siop-0.4.2.tgz#fd5b606dc1e85fe95506580fb691f384c22c2df5" + integrity sha512-uzeX530K6WxqA17X4s8jEeUb9xFymXhE+UM0uGzg7by41ohsAQ0HOoOswDxflAUap+9STXcLwgjAkQPBDPD8+w== + dependencies: + "@astronautlabs/jsonpath" "^1.1.2" + "@sphereon/did-uni-client" "^0.6.0" + "@sphereon/pex" "^2.1.2" + "@sphereon/pex-models" "^2.1.0" + "@sphereon/ssi-types" "^0.17.4" + "@sphereon/wellknown-dids-client" "^0.1.3" + cross-fetch "^3.1.8" + did-jwt "6.11.6" + did-resolver "^4.1.0" + events "^3.3.0" + language-tags "^1.0.8" + multiformats "^11.0.2" + querystring "^0.2.1" + sha.js "^2.4.11" + uint8arrays "^3.1.1" + uuid "^9.0.0" + +"@sphereon/did-uni-client@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@sphereon/did-uni-client/-/did-uni-client-0.6.0.tgz#6592e1fc514f277ddbc531fc5095a834a9813030" + integrity sha512-JDZYHR5wj49PHfI51g0+sfXzaLxIvWwad6Va42LIKcW/e9fOgjQJxpUySazWQkYYlewHhLg3GDbqMKIyIMQs6A== dependencies: - "@sphereon/ssi-types" "^0.9.0" cross-fetch "^3.1.5" + did-resolver "^4.1.0" + +"@sphereon/oid4vci-client@^0.7.3": + version "0.7.3" + resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-client/-/oid4vci-client-0.7.3.tgz#48d7ef9ec4d3ab64944f2c2bb1de9b8b5e90cfbb" + integrity sha512-9xQvpLGYqDtqjcK2R1KfCKNBJUEqhLsA5lJrxV40DQ6fTddz7lVJWommX+pRqKDRR+N6tAo80qgqzELQEzJw2w== + dependencies: + "@sphereon/oid4vci-common" "0.7.3" + "@sphereon/ssi-types" "0.17.2" + cross-fetch "^3.1.8" debug "^4.3.4" - uint8arrays "^3.1.1" + +"@sphereon/oid4vci-common@0.7.3", "@sphereon/oid4vci-common@^0.7.3": + version "0.7.3" + resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-common/-/oid4vci-common-0.7.3.tgz#188250a0a51c5a5df5424f63781f517788c4b296" + integrity sha512-rfXYXWYsa+wQ22A/IchOJSUzTr6ChuCiZYYvZB44kHEjCdQ15Ix3PKkVD6vmxaoSx7sZ9Q+LQTZbNGvx+7LpWw== + dependencies: + "@sphereon/ssi-types" "0.17.2" + cross-fetch "^3.1.8" + jwt-decode "^3.1.2" + +"@sphereon/pex-models@^2.0.3", "@sphereon/pex-models@^2.1.0", "@sphereon/pex-models@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@sphereon/pex-models/-/pex-models-2.1.1.tgz#399e529db2a7e3b9abbd7314cdba619ceb6cb758" + integrity sha512-0UX/CMwgiJSxzuBn6SLOTSKkm+uPq3dkNjl8w4EtppXp6zBB4lQMd1mJX7OifX5Bp5vPUfoz7bj2B+yyDtbZww== + +"@sphereon/pex@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-2.1.2.tgz#99ecaf9dcf62bdbaf3a24db28abc0e165051a894" + integrity sha512-x2lo4iRWfKj2NQIGVZIMhwYrCllRY7j0U9t3g0pkx3mxSUwXhQwEYAcBU+AlS5rGv1kLUXRhHDGPUwt7Y0kHgw== + dependencies: + "@astronautlabs/jsonpath" "^1.1.2" + "@sphereon/pex-models" "^2.0.3" + "@sphereon/ssi-types" "^0.15.1" + ajv "^8.12.0" + ajv-formats "^2.1.1" + jwt-decode "^3.1.2" + nanoid "^3.3.6" + string.prototype.matchall "^4.0.8" + +"@sphereon/pex@^2.1.3-unstable.6": + version "2.1.3-unstable.6" + resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-2.1.3-unstable.6.tgz#0934c07d615551bef2092673824d60f58530378a" + integrity sha512-8vyYwGGqtxfmlwJuSe2lFSI8sedFV8Y6DR1o+bqMe18cUtgJUZ2XlsDLbq9r0npC5URJQJWOUo5ZUoGWuRJfCg== + dependencies: + "@astronautlabs/jsonpath" "^1.1.2" + "@sphereon/pex-models" "^2.1.1" + "@sphereon/ssi-types" "^0.17.5" + ajv "^8.12.0" + ajv-formats "^2.1.1" + jwt-decode "^3.1.2" + nanoid "^3.3.6" + string.prototype.matchall "^4.0.8" + +"@sphereon/ssi-types@0.17.2": + version "0.17.2" + resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.17.2.tgz#d6b5e9eef1e68d8e9c846c8edf9279257e8e348d" + integrity sha512-Qo1dkISavtPIe1WKZXZGyHvquoUvdUlDI0GLzb21clKFPuxbawXdlxpCqOh6NCNRfX7ohEeCUQdEA1PNBlnKYA== + dependencies: + jwt-decode "^3.1.2" + +"@sphereon/ssi-types@^0.15.1": + version "0.15.1" + resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.15.1.tgz#120926e1b633b616026ebe3dd6e73ed6fe350110" + integrity sha512-NFpgcVHIU8YQ2OkCHpw9YVa5bIDBcfSbp0kvwC0iZa0du1tr3148fV2Xm4ilcLeRNvUKL5BbDEdHl1WuQkmoyw== + dependencies: + jwt-decode "^3.1.2" + +"@sphereon/ssi-types@^0.17.4", "@sphereon/ssi-types@^0.17.5": + version "0.17.5" + resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.17.5.tgz#7b4de0326e7c2993ab816caeef6deaea41a5f65f" + integrity sha512-hoQOkeOtshvIzNAG+HTqcKxeGssLVfwX7oILHJgs6VMb1GhR6QlqjMAxflDxZ/8Aq2R0I6fEPWmf73zAXY2X2Q== + dependencies: + jwt-decode "^3.1.2" "@sphereon/ssi-types@^0.9.0": version "0.9.0" @@ -2418,6 +2518,15 @@ dependencies: jwt-decode "^3.1.2" +"@sphereon/wellknown-dids-client@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@sphereon/wellknown-dids-client/-/wellknown-dids-client-0.1.3.tgz#4711599ed732903e9f45fe051660f925c9b508a4" + integrity sha512-TAT24L3RoXD8ocrkTcsz7HuJmgjNjdoV6IXP1p3DdaI/GqkynytXE3J1+F7vUFMRYwY5nW2RaXSgDQhrFJemaA== + dependencies: + "@sphereon/ssi-types" "^0.9.0" + cross-fetch "^3.1.5" + jwt-decode "^3.1.2" + "@stablelib/aead@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@stablelib/aead/-/aead-1.0.1.tgz#c4b1106df9c23d1b867eb9b276d8f42d5fc4c0c3" @@ -2777,6 +2886,11 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/jsonpath@^0.2.0": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@types/jsonpath/-/jsonpath-0.2.1.tgz#92a5f0328a58848449dd52249cbba270364e82e5" + integrity sha512-CmRqkJfGIthwvW6vbNeY8wI3opKqnvX8+ec83PcK14Ee3RSla1ErAFeY/gVsh42Dm/uLCnD+pkQEDDkKuBK2bQ== + "@types/long@^4.0.1": version "4.0.2" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" @@ -3142,6 +3256,13 @@ aggregate-error@^3.0.0: clean-stack "^2.0.0" indent-string "^4.0.0" +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + ajv@^6.10.0, ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -3152,6 +3273,16 @@ ajv@^6.10.0, ajv@^6.12.4: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^8.0.0, ajv@^8.12.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + anser@^1.4.9: version "1.4.10" resolved "https://registry.yarnpkg.com/anser/-/anser-1.4.10.tgz#befa3eddf282684bd03b63dcda3927aef8c2e35b" @@ -3381,6 +3512,19 @@ array.prototype.flatmap@^1.3.1: es-abstract "^1.20.4" es-shim-unscopables "^1.0.0" +arraybuffer.prototype.slice@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz#98bd561953e3e74bb34938e77647179dfe6e9f12" + integrity sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw== + dependencies: + array-buffer-byte-length "^1.0.0" + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + is-array-buffer "^3.0.2" + is-shared-array-buffer "^1.0.2" + arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -4604,6 +4748,13 @@ cross-fetch@^3.1.5: dependencies: node-fetch "2.6.7" +cross-fetch@^3.1.8: + version "3.1.8" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" + integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== + dependencies: + node-fetch "^2.6.12" + cross-fetch@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983" @@ -4718,7 +4869,7 @@ deep-extend@^0.6.0, deep-extend@~0.6.0: resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== -deep-is@^0.1.3: +deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== @@ -4740,6 +4891,15 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" +define-data-property@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.0.tgz#0db13540704e1d8d479a0656cf781267531b9451" + integrity sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g== + dependencies: + get-intrinsic "^1.2.1" + gopd "^1.0.1" + has-property-descriptors "^1.0.0" + define-lazy-prop@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" @@ -4848,7 +5008,7 @@ detect-newline@^3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== -did-jwt@^6.11.6: +did-jwt@6.11.6, did-jwt@^6.11.6: version "6.11.6" resolved "https://registry.yarnpkg.com/did-jwt/-/did-jwt-6.11.6.tgz#3eeb30d6bd01f33bfa17089574915845802a7d44" integrity sha512-OfbWknRxJuUqH6Lk0x+H1FsuelGugLbBDEwsoJnicFOntIG/A4y19fn0a8RLxaQbWQ5gXg0yDq5E2huSBiiXzw== @@ -5077,6 +5237,51 @@ es-abstract@^1.19.0, es-abstract@^1.20.4: unbox-primitive "^1.0.2" which-typed-array "^1.1.9" +es-abstract@^1.22.1: + version "1.22.2" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.22.2.tgz#90f7282d91d0ad577f505e423e52d4c1d93c1b8a" + integrity sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA== + dependencies: + array-buffer-byte-length "^1.0.0" + arraybuffer.prototype.slice "^1.0.2" + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + es-set-tostringtag "^2.0.1" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.1" + get-symbol-description "^1.0.0" + globalthis "^1.0.3" + gopd "^1.0.1" + has "^1.0.3" + has-property-descriptors "^1.0.0" + has-proto "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.5" + is-array-buffer "^3.0.2" + is-callable "^1.2.7" + is-negative-zero "^2.0.2" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.2" + is-string "^1.0.7" + is-typed-array "^1.1.12" + is-weakref "^1.0.2" + object-inspect "^1.12.3" + object-keys "^1.1.1" + object.assign "^4.1.4" + regexp.prototype.flags "^1.5.1" + safe-array-concat "^1.0.1" + safe-regex-test "^1.0.0" + string.prototype.trim "^1.2.8" + string.prototype.trimend "^1.0.7" + string.prototype.trimstart "^1.0.7" + typed-array-buffer "^1.0.0" + typed-array-byte-length "^1.0.0" + typed-array-byte-offset "^1.0.0" + typed-array-length "^1.0.4" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.11" + es-set-tostringtag@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz#338d502f6f674301d710b80c8592de8a15f09cd8" @@ -5153,6 +5358,18 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== +escodegen@^1.8.1: + version "1.14.3" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" + integrity sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw== + dependencies: + esprima "^4.0.1" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + eslint-config-prettier@^8.3.0: version "8.8.0" resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348" @@ -5299,7 +5516,12 @@ espree@^9.5.1: acorn-jsx "^5.3.2" eslint-visitor-keys "^3.4.0" -esprima@^4.0.0, esprima@~4.0.0: +esprima@1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-1.2.2.tgz#76a0fd66fcfe154fd292667dc264019750b1657b" + integrity sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A== + +esprima@^4.0.0, esprima@^4.0.1, esprima@~4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== @@ -5318,7 +5540,7 @@ esrecurse@^4.3.0: dependencies: estraverse "^5.2.0" -estraverse@^4.1.1: +estraverse@^4.1.1, estraverse@^4.2.0: version "4.3.0" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== @@ -5567,12 +5789,12 @@ fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-sta resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-levenshtein@^2.0.6: +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -fast-text-encoding@^1.0.3: +fast-text-encoding@^1.0.3, fast-text-encoding@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== @@ -5913,6 +6135,16 @@ function.prototype.name@^1.1.5: es-abstract "^1.19.0" functions-have-names "^1.2.2" +function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" + functions-have-names@^1.2.2, functions-have-names@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" @@ -5994,6 +6226,16 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@ has "^1.0.3" has-symbols "^1.0.3" +get-intrinsic@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.1.tgz#d295644fed4505fc9cde952c37ee12b477a83d82" + integrity sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-proto "^1.0.1" + has-symbols "^1.0.3" + get-package-type@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" @@ -6583,7 +6825,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -7000,6 +7242,13 @@ is-typed-array@^1.1.10, is-typed-array@^1.1.9: gopd "^1.0.1" has-tostringtag "^1.0.0" +is-typed-array@^1.1.12: + version "1.1.12" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a" + integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg== + dependencies: + which-typed-array "^1.1.11" + is-unicode-supported@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" @@ -7034,6 +7283,11 @@ isarray@1.0.0, isarray@~1.0.0: resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" @@ -7644,6 +7898,11 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + json-stable-stringify-without-jsonify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" @@ -7704,6 +7963,15 @@ jsonparse@^1.2.0, jsonparse@^1.3.1: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== +jsonpath@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/jsonpath/-/jsonpath-1.1.1.tgz#0ca1ed8fb65bb3309248cc9d5466d12d5b0b9901" + integrity sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w== + dependencies: + esprima "1.2.2" + static-eval "2.0.2" + underscore "1.12.1" + just-diff-apply@^5.2.0: version "5.5.0" resolved "https://registry.yarnpkg.com/just-diff-apply/-/just-diff-apply-5.5.0.tgz#771c2ca9fa69f3d2b54e7c3f5c1dfcbcc47f9f0f" @@ -7761,6 +8029,18 @@ ky@^0.25.1: resolved "https://registry.yarnpkg.com/ky/-/ky-0.25.1.tgz#0df0bd872a9cc57e31acd5dbc1443547c881bfbc" integrity sha512-PjpCEWlIU7VpiMVrTwssahkYXX1by6NCT0fhTUX34F3DTinARlgMpriuroolugFPcMgpPWrOW4mTb984Qm1RXA== +language-subtag-registry@^0.3.20: + version "0.3.22" + resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d" + integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w== + +language-tags@^1.0.8: + version "1.0.9" + resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.9.tgz#1ffdcd0ec0fafb4b1be7f8b11f306ad0f9c08777" + integrity sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA== + dependencies: + language-subtag-registry "^0.3.20" + lerna@^6.5.1: version "6.6.1" resolved "https://registry.yarnpkg.com/lerna/-/lerna-6.6.1.tgz#4897171aed64e244a2d0f9000eef5c5b228f9332" @@ -7856,6 +8136,14 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA== + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + libnpmaccess@6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/libnpmaccess/-/libnpmaccess-6.0.3.tgz#473cc3e4aadb2bc713419d92e45d23b070d8cded" @@ -8800,6 +9088,11 @@ msrcrypto@^1.5.6: resolved "https://registry.yarnpkg.com/msrcrypto/-/msrcrypto-1.5.8.tgz#be419be4945bf134d8af52e9d43be7fa261f4a1c" integrity sha512-ujZ0TRuozHKKm6eGbKHfXef7f+esIhEckmThVnz7RNyiOJd7a6MXj2JGBoL9cnPDW+JMG16MoTUh5X+XXjI66Q== +multiformats@^11.0.2: + version "11.0.2" + resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-11.0.2.tgz#b14735efc42cd8581e73895e66bebb9752151b60" + integrity sha512-b5mYMkOkARIuVZCpvijFj9a6m5wMVLC7cf/jIPd5D/ARDOfLC5+IFkbgDXQgcU2goIsTD/O9NY4DI/Mt4OGvlg== + multiformats@^9.4.2, multiformats@^9.6.5, multiformats@^9.9.0: version "9.9.0" resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37" @@ -8826,6 +9119,11 @@ nan@^2.11.1: resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== +nanoid@^3.3.6: + version "3.3.6" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" + integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -9527,6 +9825,18 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + optionator@^0.9.1: version "0.9.1" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" @@ -9911,6 +10221,11 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w== + prettier-linter-helpers@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b" @@ -10142,6 +10457,11 @@ query-string@^7.0.1: split-on-first "^1.0.0" strict-uri-encode "^2.0.0" +querystring@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd" + integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg== + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -10518,6 +10838,15 @@ regexp.prototype.flags@^1.4.3: define-properties "^1.2.0" functions-have-names "^1.2.3" +regexp.prototype.flags@^1.5.0, regexp.prototype.flags@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz#90ce989138db209f81492edd734183ce99f9677e" + integrity sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + set-function-name "^2.0.0" + regexpu-core@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.3.2.tgz#11a2b06884f3527aec3e93dbbf4a3b958a95546b" @@ -10552,6 +10881,11 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" @@ -10685,6 +11019,16 @@ rxjs@^7.2.0, rxjs@^7.5.5, rxjs@^7.8.0: dependencies: tslib "^2.1.0" +safe-array-concat@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.0.1.tgz#91686a63ce3adbea14d61b14c99572a8ff84754c" + integrity sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + isarray "^2.0.5" + safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -10805,6 +11149,15 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== +set-function-name@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.1.tgz#12ce38b7954310b9f61faa12701620a0c882793a" + integrity sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA== + dependencies: + define-data-property "^1.0.1" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.0" + set-value@^2.0.0, set-value@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" @@ -10820,6 +11173,14 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== +sha.js@^2.4.11: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" @@ -11115,6 +11476,13 @@ stacktrace-parser@^0.1.3: dependencies: type-fest "^0.7.1" +static-eval@2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/static-eval/-/static-eval-2.0.2.tgz#2d1759306b1befa688938454c546b7871f806a42" + integrity sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg== + dependencies: + escodegen "^1.8.1" + static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" @@ -11169,6 +11537,21 @@ string-width@^1.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string.prototype.matchall@^4.0.8: + version "4.0.10" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz#a1553eb532221d4180c51581d6072cd65d1ee100" + integrity sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + internal-slot "^1.0.5" + regexp.prototype.flags "^1.5.0" + set-function-name "^2.0.0" + side-channel "^1.0.4" + string.prototype.trim@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.7.tgz#a68352740859f6893f14ce3ef1bb3037f7a90533" @@ -11178,6 +11561,15 @@ string.prototype.trim@^1.2.7: define-properties "^1.1.4" es-abstract "^1.20.4" +string.prototype.trim@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz#f9ac6f8af4bd55ddfa8895e6aea92a96395393bd" + integrity sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + string.prototype.trimend@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz#c4a27fa026d979d79c04f17397f250a462944533" @@ -11187,6 +11579,15 @@ string.prototype.trimend@^1.0.6: define-properties "^1.1.4" es-abstract "^1.20.4" +string.prototype.trimend@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz#1bb3afc5008661d73e2dc015cd4853732d6c471e" + integrity sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + string.prototype.trimstart@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz#e90ab66aa8e4007d92ef591bbf3cd422c56bdcf4" @@ -11196,6 +11597,15 @@ string.prototype.trimstart@^1.0.6: define-properties "^1.1.4" es-abstract "^1.20.4" +string.prototype.trimstart@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz#d4cdb44b83a4737ffbac2d406e405d43d0184298" + integrity sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -11669,6 +12079,13 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg== + dependencies: + prelude-ls "~1.1.2" + type-detect@4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" @@ -11737,6 +12154,36 @@ type@^2.7.2: resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== +typed-array-buffer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz#18de3e7ed7974b0a729d3feecb94338d1472cd60" + integrity sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + is-typed-array "^1.1.10" + +typed-array-byte-length@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz#d787a24a995711611fb2b87a4052799517b230d0" + integrity sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA== + dependencies: + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + +typed-array-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz#cbbe89b51fdef9cd6aaf07ad4707340abbc4ea0b" + integrity sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + has-proto "^1.0.1" + is-typed-array "^1.1.10" + typed-array-length@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.4.tgz#89d83785e5c4098bec72e08b319651f0eac9c1bb" @@ -11796,6 +12243,11 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" +underscore@1.12.1: + version "1.12.1" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.1.tgz#7bb8cc9b3d397e201cf8553336d262544ead829e" + integrity sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" @@ -12111,6 +12563,17 @@ which-module@^2.0.0: resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== +which-typed-array@^1.1.11: + version "1.1.11" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a" + integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew== + dependencies: + available-typed-arrays "^1.0.5" + call-bind "^1.0.2" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.0" + which-typed-array@^1.1.9: version "1.1.9" resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" @@ -12156,6 +12619,11 @@ word-wrap@^1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.4.tgz#cb4b50ec9aca570abd1f52f33cd45b6c61739a9f" integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== +word-wrap@~1.2.3: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + wordwrap@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" From 36d417f6cc2ef90010e2a571a55f28a03dc4a03f Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 10 Oct 2023 12:19:35 +0200 Subject: [PATCH 002/115] refactor: remove unused import Signed-off-by: Martin Auer --- packages/openid4vc-client/src/utils/metadata.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/openid4vc-client/src/utils/metadata.ts b/packages/openid4vc-client/src/utils/metadata.ts index fd659be10b..2ae316632b 100644 --- a/packages/openid4vc-client/src/utils/metadata.ts +++ b/packages/openid4vc-client/src/utils/metadata.ts @@ -4,7 +4,6 @@ import type { CredentialsSupportedDisplay, CredentialSupported, EndpointMetadata, - EndpointMetadataResult, IssuerCredentialSubject, IssuerMetadataV1_0_08, MetadataDisplay, From fecbdc921f7dd25de47a11652d2d50b492c12aff Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 10 Oct 2023 12:20:19 +0200 Subject: [PATCH 003/115] refactor: and add some TODOs Signed-off-by: Martin Auer --- .../src/OpenId4VcClientService.ts | 55 +++++++++++-------- 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/packages/openid4vc-client/src/OpenId4VcClientService.ts b/packages/openid4vc-client/src/OpenId4VcClientService.ts index 21f59d0d0b..e8d7247e2f 100644 --- a/packages/openid4vc-client/src/OpenId4VcClientService.ts +++ b/packages/openid4vc-client/src/OpenId4VcClientService.ts @@ -21,6 +21,7 @@ import type { CredentialSupported, Jwt, OpenIDResponse, + ProofOfPossessionCallbacks, } from '@sphereon/oid4vci-common' import { @@ -115,6 +116,7 @@ export class OpenId4VcClientService { uri: options.initiationUri, flowType: AuthzFlowType.AUTHORIZATION_CODE_FLOW, }) + const codeVerifier = this.generateCodeVerifier() const codeVerifierSha256 = Hasher.hash(TypedArrayEncoder.fromString(codeVerifier), 'sha2-256') const base64Url = TypedArrayEncoder.toBase64URL(codeVerifierSha256) @@ -162,8 +164,19 @@ export class OpenId4VcClientService { const client = await OpenID4VCIClient.fromURI({ uri: options.issuerUri, flowType, + retrieveServerMetadata: false, }) + const serverMetadata = await client.retrieveServerMetadata() + + this.logger.info('Fetched server metadata', { + issuer: serverMetadata.issuer, + credentialEndpoint: serverMetadata.credential_endpoint, + tokenEndpoint: serverMetadata.token_endpoint, + }) + + this.logger.debug('Full server metadata', serverMetadata) + // acquire the access token // NOTE: only scope based flow is supported for authorized flow. However there's not clear mapping between // the scope property and which credential to request (this is out of scope of the spec), so it will still @@ -177,17 +190,7 @@ export class OpenId4VcClientService { codeVerifier: options.codeVerifier, redirectUri: options.redirectUri, }) - : await client.acquireAccessToken({}) - - const serverMetadata = await client.retrieveServerMetadata() - - this.logger.info('Fetched server metadata', { - issuer: serverMetadata.issuer, - credentialEndpoint: serverMetadata.credential_endpoint, - tokenEndpoint: serverMetadata.token_endpoint, - }) - - this.logger.debug('Full server metadata', serverMetadata) + : await client.acquireAccessToken({}) // TODO: PIN // Loop through all the credentialTypes in the credential offer for (const offeredCredential of this.getOfferedCredentialsWithMetadata(client)) { @@ -195,7 +198,7 @@ export class OpenId4VcClientService { isInlineCredentialOffer(offeredCredential) ? offeredCredential.inlineCredentialOffer.format : offeredCredential.credentialSupported.format - ) as SupportedCredentialFormats + ) as SupportedCredentialFormats // TODO: can we remove the cast? // TODO: support inline credential offers. Not clear to me how to determine the did method / alg, etc.. if (offeredCredential.type === OfferedCredentialType.InlineCredentialOffer) { @@ -205,6 +208,7 @@ export class OpenId4VcClientService { const supportedCredentialMetadata = offeredCredential.credentialSupported // FIXME + // TODO: that is not a must v11 could end in the same way // If the credential id ends with the format, it is a v8 credential supported that has been // split into multiple entries (each entry can now only have one format). For now we continue // as assume there will be another entry with the correct format. @@ -222,12 +226,15 @@ export class OpenId4VcClientService { proofOfPossessionVerificationMethodResolver: options.proofOfPossessionVerificationMethodResolver, }) + const callbacks: ProofOfPossessionCallbacks = { + signCallback: this.signCallback(agentContext, verificationMethod), + // TODO: verify callback + } + // Create the proof of possession const proofInput = await ProofOfPossessionBuilder.fromAccessTokenResponse({ accessTokenResponse: accessToken, - callbacks: { - signCallback: this.signCallback(agentContext, verificationMethod), - }, + callbacks, version: client.version(), }) .withEndpointMetadata(serverMetadata) @@ -239,14 +246,15 @@ export class OpenId4VcClientService { this.logger.debug('Generated JWS', proofInput) // Acquire the credential - const credentialRequestClient = ( - await CredentialRequestClientBuilder.fromURI({ - uri: options.issuerUri, - metadata: serverMetadata, - }) - ) - .withTokenFromResponse(accessToken) - .build() + const credentialRequestClient = // TODO: don't use the uri not actual anymore https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-08.html + ( + await CredentialRequestClientBuilder.fromURI({ + uri: options.issuerUri, + metadata: serverMetadata, + }) + ) + .withTokenFromResponse(accessToken) + .build() let credentialResponse: OpenIDResponse @@ -371,6 +379,7 @@ export class OpenId4VcClientService { return { verificationMethod, signatureAlgorithm } } + // TODO: i cannot view this // todo https://sphereon.atlassian.net/browse/VDX-184 /** * Returns all entries from the credential offer. This includes both 'id' entries that reference a supported credential in the issuer metadata, From feba10ea5cef59c31769c2f6937faeec47697ad2 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 10 Oct 2023 12:47:23 +0200 Subject: [PATCH 004/115] feat: crete openid4vc-holder package Signed-off-by: Martin Auer --- packages/openid4vc-holder/README.md | 167 ++++ packages/openid4vc-holder/jest.config.ts | 14 + packages/openid4vc-holder/package.json | 43 + .../src/OpenId4VcHolderApi.ts | 53 ++ .../src/OpenId4VcHolderModule.ts | 31 + .../src/OpenId4VcHolderService.ts | 733 ++++++++++++++++++ .../src/OpenId4VcHolderServiceOptions.ts | 185 +++++ packages/openid4vc-holder/src/index.ts | 21 + .../openid4vc-holder/src/utils/Formats.ts | 41 + .../src/utils/IssuerMetadataUtils.ts | 113 +++ .../__tests__/claimFormatMapping.test.ts | 45 ++ .../src/utils/claimFormatMapping.ts | 40 + packages/openid4vc-holder/src/utils/index.ts | 2 + .../openid4vc-holder/src/utils/metadata.ts | 69 ++ .../tests/OpenId4VcClientModule.test.ts | 26 + packages/openid4vc-holder/tests/fixtures.ts | 460 +++++++++++ .../tests/openid4vc-client.e2e.test.ts | 483 ++++++++++++ packages/openid4vc-holder/tests/setup.ts | 1 + packages/openid4vc-holder/tsconfig.build.json | 8 + packages/openid4vc-holder/tsconfig.json | 7 + 20 files changed, 2542 insertions(+) create mode 100644 packages/openid4vc-holder/README.md create mode 100644 packages/openid4vc-holder/jest.config.ts create mode 100644 packages/openid4vc-holder/package.json create mode 100644 packages/openid4vc-holder/src/OpenId4VcHolderApi.ts create mode 100644 packages/openid4vc-holder/src/OpenId4VcHolderModule.ts create mode 100644 packages/openid4vc-holder/src/OpenId4VcHolderService.ts create mode 100644 packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts create mode 100644 packages/openid4vc-holder/src/index.ts create mode 100644 packages/openid4vc-holder/src/utils/Formats.ts create mode 100644 packages/openid4vc-holder/src/utils/IssuerMetadataUtils.ts create mode 100644 packages/openid4vc-holder/src/utils/__tests__/claimFormatMapping.test.ts create mode 100644 packages/openid4vc-holder/src/utils/claimFormatMapping.ts create mode 100644 packages/openid4vc-holder/src/utils/index.ts create mode 100644 packages/openid4vc-holder/src/utils/metadata.ts create mode 100644 packages/openid4vc-holder/tests/OpenId4VcClientModule.test.ts create mode 100644 packages/openid4vc-holder/tests/fixtures.ts create mode 100644 packages/openid4vc-holder/tests/openid4vc-client.e2e.test.ts create mode 100644 packages/openid4vc-holder/tests/setup.ts create mode 100644 packages/openid4vc-holder/tsconfig.build.json create mode 100644 packages/openid4vc-holder/tsconfig.json diff --git a/packages/openid4vc-holder/README.md b/packages/openid4vc-holder/README.md new file mode 100644 index 0000000000..1cb88b2763 --- /dev/null +++ b/packages/openid4vc-holder/README.md @@ -0,0 +1,167 @@ +

+
+ Hyperledger Aries logo +

+

Aries Framework JavaScript Open ID Connect For Verifiable Credentials Client Module

+

+ License + typescript + @aries-framework/openid4vc-holder version + +

+
+ +Open ID Connect For Verifiable Credentials Holder Module for [Aries Framework JavaScript](https://github.com/hyperledger/aries-framework-javascript). + +### Installation + +Make sure you have set up the correct version of Aries Framework JavaScript according to the AFJ repository. + +```sh +yarn add @aries-framework/openid4vc-holder +``` + +### Quick start + +#### Requirements + +Before a credential can be requested, you need the issuer URI. This URI starts with `openid-initiate-issuance://` and is provided by the issuer. The issuer URI is commonly acquired by scanning a QR code. + +#### Module registration + +In order to get this module to work, we need to inject it into the agent. This makes the module's functionality accessible through the agent's `modules` api. + +```ts +import { OpenId4VcHolderModule } from '@aries-framework/openid4vc-holder' + +const agent = new Agent({ + config: { + /* config */ + }, + dependencies: agentDependencies, + modules: { + openId4VcHolder: new OpenId4VcHolderModule(), + /* other custom modules */ + }, +}) + +await agent.initialize() +``` + +How the module is injected and the agent has been initialized, you can access the module's functionality through `agent.modules.openId4VcHolder`. + +#### Preparing a DID + +In order to request a credential, you'll need to provide a DID that the issuer will use for setting the credential subject. In the following snippet we create one for the sake of the example, but this can be any DID that has a _authentication verification method_ with key type `Ed25519`. + +```ts +// first we create the DID +const did = await agent.dids.create({ + method: 'key', + options: { + keyType: KeyType.Ed25519, + }, +}) + +// next we do some assertions and extract the key identifier (kid) + +if ( + !did.didState.didDocument || + !did.didState.didDocument.authentication || + did.didState.didDocument.authentication.length === 0 +) { + throw new Error("Error creating did document, or did document has no 'authentication' verificationMethods") +} + +const [verificationMethod] = did.didState.didDocument.authentication +const kid = typeof verificationMethod === 'string' ? verificationMethod : verificationMethod.id +``` + +#### Requesting the credential (Pre-Authorized) + +Now a credential issuance can be requested as follows. + +```ts +const w3cCredentialRecord = await agent.modules.openId4VcHolder.requestCredentialPreAuthorized({ + issuerUri, + kid, + checkRevocationState: false, +}) + +console.log(w3cCredentialRecord) +``` + +#### Full example + +```ts +import { OpenId4VcHolderModule } from '@aries-framework/openid4vc-holder' +import { agentDependencies } from '@aries-framework/node' // use @aries-framework/react-native for React Native +import { Agent, KeyDidCreateOptions } from '@aries-framework/core' + +const run = async () => { + const issuerUri = '' // The obtained issuer URI + + // Create the Agent + const agent = new Agent({ + config: { + /* config */ + }, + dependencies: agentDependencies, + modules: { + openId4VcHolder: new OpenId4VcHolderModule(), + /* other custom modules */ + }, + }) + + // Initialize the Agent + await agent.initialize() + + // Create a DID + const did = await agent.dids.create({ + method: 'key', + options: { + keyType: KeyType.Ed25519, + }, + }) + + // Assert DIDDocument is valid + if ( + !did.didState.didDocument || + !did.didState.didDocument.authentication || + did.didState.didDocument.authentication.length === 0 + ) { + throw new Error("Error creating did document, or did document has no 'authentication' verificationMethods") + } + + // Extract key identified (kid) for authentication verification method + const [verificationMethod] = did.didState.didDocument.authentication + const kid = typeof verificationMethod === 'string' ? verificationMethod : verificationMethod.id + + // Request the credential + const w3cCredentialRecord = await agent.modules.openId4VcHolder.requestCredentialPreAuthorized({ + issuerUri, + kid, + checkRevocationState: false, + }) + + // Log the received credential + console.log(w3cCredentialRecord) +} +``` diff --git a/packages/openid4vc-holder/jest.config.ts b/packages/openid4vc-holder/jest.config.ts new file mode 100644 index 0000000000..8641cf4d67 --- /dev/null +++ b/packages/openid4vc-holder/jest.config.ts @@ -0,0 +1,14 @@ +import type { Config } from '@jest/types' + +import base from '../../jest.config.base' + +import packageJson from './package.json' + +const config: Config.InitialOptions = { + ...base, + + displayName: packageJson.name, + setupFilesAfterEnv: ['./tests/setup.ts'], +} + +export default config diff --git a/packages/openid4vc-holder/package.json b/packages/openid4vc-holder/package.json new file mode 100644 index 0000000000..3eac40e571 --- /dev/null +++ b/packages/openid4vc-holder/package.json @@ -0,0 +1,43 @@ +{ + "name": "@aries-framework/openid4vc-holder", + "main": "build/index", + "types": "build/index", + "version": "0.4.2", + "files": [ + "build" + ], + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "homepage": "https://github.com/hyperledger/aries-framework-javascript/tree/main/packages/openid4vc-holder", + "repository": { + "type": "git", + "url": "https://github.com/hyperledger/aries-framework-javascript", + "directory": "packages/openid4vc-holder" + }, + "scripts": { + "build": "yarn run clean && yarn run compile", + "clean": "rimraf ./build", + "compile": "tsc -p tsconfig.build.json", + "prepublishOnly": "yarn run build", + "test": "jest" + }, + "dependencies": { + "@aries-framework/core": "0.4.2", + "@sphereon/oid4vci-client": "^0.7.3", + "@sphereon/oid4vci-common": "^0.7.3", + "@sphereon/ssi-types": "^0.17.5", + "@stablelib/random": "^1.0.2", + "fast-text-encoding": "^1.0.6" + }, + "devDependencies": { + "@aries-framework/askar": "0.4.2", + "@aries-framework/node": "0.4.2", + "@hyperledger/aries-askar-nodejs": "^0.1.0", + "@types/jsonpath": "^0.2.0", + "nock": "^13.3.0", + "rimraf": "^4.4.0", + "typescript": "~4.9.5" + } +} diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts new file mode 100644 index 0000000000..c2c14d6ba6 --- /dev/null +++ b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts @@ -0,0 +1,53 @@ +import type { + GenerateAuthorizationUrlOptions, + PreAuthCodeFlowOptions, + AuthCodeFlowOptions, +} from './OpenId4VcHolderServiceOptions' +import type { W3cCredentialRecord } from '@aries-framework/core' + +import { injectable, AgentContext } from '@aries-framework/core' + +import { OpenId4VcHolderService } from './OpenId4VcHolderService' +import { AuthFlowType } from './OpenId4VcHolderServiceOptions' + +/** + * @public + */ +@injectable() +export class OpenId4VcHolderApi { + private agentContext: AgentContext + private openId4VcHolderService: OpenId4VcHolderService + + public constructor(agentContext: AgentContext, openId4VcHolderService: OpenId4VcHolderService) { + this.agentContext = agentContext + this.openId4VcHolderService = openId4VcHolderService + } + + public async requestCredentialUsingPreAuthorizedCode( + options: PreAuthCodeFlowOptions + ): Promise { + // set defaults + const verifyRevocationState = options.verifyCredentialStatus ?? true + + return this.openId4VcHolderService.requestCredential(this.agentContext, { + ...options, + verifyCredentialStatus: verifyRevocationState, + flowType: AuthFlowType.PreAuthorizedCodeFlow, + }) + } + + public async requestCredentialUsingAuthorizationCode(options: AuthCodeFlowOptions): Promise { + // set defaults + const checkRevocationState = options.verifyCredentialStatus ?? true + + return this.openId4VcHolderService.requestCredential(this.agentContext, { + ...options, + verifyCredentialStatus: checkRevocationState, + flowType: AuthFlowType.AuthorizationCodeFlow, + }) + } + + public async generateAuthorizationUrl(options: GenerateAuthorizationUrlOptions) { + return this.openId4VcHolderService.generateAuthorizationUrl(options) + } +} diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts b/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts new file mode 100644 index 0000000000..6468aa07e3 --- /dev/null +++ b/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts @@ -0,0 +1,31 @@ +import type { DependencyManager, Module } from '@aries-framework/core' + +import { AgentConfig } from '@aries-framework/core' + +import { OpenId4VcHolderApi } from './OpenId4VcHolderApi' +import { OpenId4VcHolderService } from './OpenId4VcHolderService' + +/** + * @public + */ +export class OpenId4VcHolderModule implements Module { + public readonly api = OpenId4VcHolderApi + + /** + * Registers the dependencies of the question answer module on the dependency manager. + */ + public register(dependencyManager: DependencyManager) { + // Warn about experimental module + dependencyManager + .resolve(AgentConfig) + .logger.warn( + "The '@aries-framework/openid4vc-holder' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." + ) + + // Api + dependencyManager.registerContextScoped(OpenId4VcHolderApi) + + // Services + dependencyManager.registerSingleton(OpenId4VcHolderService) + } +} diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderService.ts b/packages/openid4vc-holder/src/OpenId4VcHolderService.ts new file mode 100644 index 0000000000..31bc7c1ee1 --- /dev/null +++ b/packages/openid4vc-holder/src/OpenId4VcHolderService.ts @@ -0,0 +1,733 @@ +import type { + GenerateAuthorizationUrlOptions, + RequestCredentialOptions, + ProofOfPossessionVerificationMethodResolver, + SupportedCredentialFormats, + ProofOfPossessionRequirements, +} from './OpenId4VcHolderServiceOptions' +import type { OpenIdCredentialFormatProfile } from './utils' +import type { + AgentContext, + W3cVerifiableCredential, + VerificationMethod, + JwaSignatureAlgorithm, + W3cVerifyCredentialResult, +} from '@aries-framework/core' +import type { + CredentialOfferFormat, + CredentialOfferPayloadV1_0_08, + CredentialOfferRequestWithBaseUrl, + CredentialResponse, + CredentialSupported, + Jwt, + OpenIDResponse, + ProofOfPossessionCallbacks, +} from '@sphereon/oid4vci-common' + +import { + W3cCredentialRecord, + ClaimFormat, + getJwkClassFromJwaSignatureAlgorithm, + W3cJwtVerifiableCredential, + AriesFrameworkError, + getKeyFromVerificationMethod, + Hasher, + inject, + injectable, + InjectionSymbols, + JsonEncoder, + JsonTransformer, + TypedArrayEncoder, + W3cJsonLdVerifiableCredential, + getJwkFromKey, + getSupportedVerificationMethodTypesFromKeyType, + getJwkClassFromKeyType, + parseDid, + SignatureSuiteRegistry, + JwsService, + Logger, + W3cCredentialService, + W3cCredentialRepository, +} from '@aries-framework/core' +import { CredentialRequestClientBuilder, OpenID4VCIClient, ProofOfPossessionBuilder } from '@sphereon/oid4vci-client' +import { AuthzFlowType, CodeChallengeMethod, OpenId4VCIVersion } from '@sphereon/oid4vci-common' +import { randomStringForEntropy } from '@stablelib/random' + +import { supportedCredentialFormats, AuthFlowType } from './OpenId4VcHolderServiceOptions' +import { setOpenId4VcCredentialMetadata, fromOpenIdCredentialFormatProfileToDifClaimFormat } from './utils' +import { getUniformFormat } from './utils/Formats' +import { getSupportedCredentials } from './utils/IssuerMetadataUtils' + +/** + * The type of a credential offer entry. For each item in `credentials` array, the type MUST be one of the following: + * - CredentialSupported, when the value is a string and points to a credential from the `credentials_supported` array. + * - InlineCredentialOffer, when the value is a JSON object that represents an inline credential offer. + */ +export enum OfferedCredentialType { + CredentialSupported = 'CredentialSupported', + InlineCredentialOffer = 'InlineCredentialOffer', +} + +export type OfferedCredentialsWithMetadata = + | { credentialSupported: CredentialSupported; type: OfferedCredentialType.CredentialSupported } + | { inlineCredentialOffer: CredentialOfferFormat; type: OfferedCredentialType.InlineCredentialOffer } + +const flowTypeMapping = { + [AuthFlowType.AuthorizationCodeFlow]: AuthzFlowType.AUTHORIZATION_CODE_FLOW, + [AuthFlowType.PreAuthorizedCodeFlow]: AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW, +} + +/** + * @internal + */ +@injectable() +export class OpenId4VcHolderService { + private logger: Logger + private w3cCredentialService: W3cCredentialService + private w3cCredentialRepository: W3cCredentialRepository + private jwsService: JwsService + + public constructor( + @inject(InjectionSymbols.Logger) logger: Logger, + w3cCredentialService: W3cCredentialService, + w3cCredentialRepository: W3cCredentialRepository, + jwsService: JwsService + ) { + this.w3cCredentialService = w3cCredentialService + this.w3cCredentialRepository = w3cCredentialRepository + this.jwsService = jwsService + this.logger = logger + } + + private generateCodeVerifier(): string { + return randomStringForEntropy(256) + } + + public async generateAuthorizationUrl(options: GenerateAuthorizationUrlOptions) { + this.logger.debug('Generating authorization url') + + if (!options.scope || options.scope.length === 0) { + throw new AriesFrameworkError( + 'Only scoped based authorization requests are supported at this time. Please provide at least one scope' + ) + } + + const client = await OpenID4VCIClient.fromURI({ + uri: options.initiationUri, + flowType: AuthzFlowType.AUTHORIZATION_CODE_FLOW, + }) + + const codeVerifier = this.generateCodeVerifier() + const codeVerifierSha256 = Hasher.hash(TypedArrayEncoder.fromString(codeVerifier), 'sha2-256') + const base64Url = TypedArrayEncoder.toBase64URL(codeVerifierSha256) + + this.logger.debug('Converted code_verifier to code_challenge', { + codeVerifier: codeVerifier, + sha256: codeVerifierSha256.toString(), + base64Url: base64Url, + }) + + const authorizationUrl = client.createAuthorizationRequestUrl({ + clientId: options.clientId, + codeChallengeMethod: CodeChallengeMethod.SHA256, + codeChallenge: base64Url, + redirectUri: options.redirectUri, + scope: options.scope?.join(' '), + }) + + return { + authorizationUrl, + codeVerifier, + } + } + + public async requestCredential(agentContext: AgentContext, options: RequestCredentialOptions) { + const receivedCredentials: W3cCredentialRecord[] = [] + const supportedJwaSignatureAlgorithms = this.getSupportedJwaSignatureAlgorithms(agentContext) + + const allowedProofOfPossessionSignatureAlgorithms = options.allowedProofOfPossessionSignatureAlgorithms + ? options.allowedProofOfPossessionSignatureAlgorithms.filter((algorithm) => + supportedJwaSignatureAlgorithms.includes(algorithm) + ) + : supportedJwaSignatureAlgorithms + + // Take the allowed credential formats from the options or use the default + const allowedCredentialFormats = options.allowedCredentialFormats ?? supportedCredentialFormats + + const flowType = flowTypeMapping[options.flowType] + if (!flowType) { + throw new AriesFrameworkError( + `Unsupported flowType ${options.flowType}. Valid values are ${Object.values(AuthFlowType).join(', ')}` + ) + } + + const client = await OpenID4VCIClient.fromURI({ + uri: options.issuerUri, + flowType, + retrieveServerMetadata: false, + }) + + const serverMetadata = await client.retrieveServerMetadata() + + this.logger.info('Fetched server metadata', { + issuer: serverMetadata.issuer, + credentialEndpoint: serverMetadata.credential_endpoint, + tokenEndpoint: serverMetadata.token_endpoint, + }) + + this.logger.debug('Full server metadata', serverMetadata) + + // acquire the access token + // NOTE: only scope based flow is supported for authorized flow. However there's not clear mapping between + // the scope property and which credential to request (this is out of scope of the spec), so it will still + // just request all credentials that have been offered in the credential offer. We may need to add some extra + // input properties that allows to define the credential type(s) to request. + const accessToken = + options.flowType === AuthFlowType.AuthorizationCodeFlow + ? await client.acquireAccessToken({ + clientId: options.clientId, + code: options.authorizationCode, + codeVerifier: options.codeVerifier, + redirectUri: options.redirectUri, + }) + : await client.acquireAccessToken({}) // TODO: PIN + + // Loop through all the credentialTypes in the credential offer + for (const offeredCredential of this.getOfferedCredentialsWithMetadata(client)) { + const format = ( + isInlineCredentialOffer(offeredCredential) + ? offeredCredential.inlineCredentialOffer.format + : offeredCredential.credentialSupported.format + ) as SupportedCredentialFormats // TODO: can we remove the cast? + + // TODO: support inline credential offers. Not clear to me how to determine the did method / alg, etc.. + if (offeredCredential.type === OfferedCredentialType.InlineCredentialOffer) { + // Check if the format is supported/allowed + if (!allowedCredentialFormats.includes(format)) continue + } else { + const supportedCredentialMetadata = offeredCredential.credentialSupported + + // FIXME + // TODO: that is not a must v11 could end in the same way + // If the credential id ends with the format, it is a v8 credential supported that has been + // split into multiple entries (each entry can now only have one format). For now we continue + // as assume there will be another entry with the correct format. + if (supportedCredentialMetadata.id?.endsWith(`-${supportedCredentialMetadata.format}`)) { + const uniformFormat = getUniformFormat(supportedCredentialMetadata.format) as SupportedCredentialFormats + if (!allowedCredentialFormats.includes(uniformFormat)) continue + } + } + + // Get all options for the credential request (such as which kid to use, the signature algorithm, etc) + const { verificationMethod, signatureAlgorithm } = await this.getCredentialRequestOptions(agentContext, { + allowedCredentialFormats, + allowedProofOfPossessionSignatureAlgorithms, + offeredCredentialWithMetadata: offeredCredential, + proofOfPossessionVerificationMethodResolver: options.proofOfPossessionVerificationMethodResolver, + }) + + const callbacks: ProofOfPossessionCallbacks = { + signCallback: this.signCallback(agentContext, verificationMethod), + // TODO: verify callback + } + + // Create the proof of possession + const proofInput = await ProofOfPossessionBuilder.fromAccessTokenResponse({ + accessTokenResponse: accessToken, + callbacks, + version: client.version(), + }) + .withEndpointMetadata(serverMetadata) + .withAlg(signatureAlgorithm) + .withClientId(verificationMethod.controller) + .withKid(verificationMethod.id) + .build() + + this.logger.debug('Generated JWS', proofInput) + + // Acquire the credential + const credentialRequestClient = // TODO: don't use the uri not actual anymore https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-08.html + ( + await CredentialRequestClientBuilder.fromURI({ + uri: options.issuerUri, + metadata: serverMetadata, + }) + ) + .withTokenFromResponse(accessToken) + .build() + + let credentialResponse: OpenIDResponse + + if (isInlineCredentialOffer(offeredCredential)) { + credentialResponse = await credentialRequestClient.acquireCredentialsUsingProof({ + proofInput, + credentialTypes: offeredCredential.inlineCredentialOffer.types, + format: offeredCredential.inlineCredentialOffer.format, + }) + } else { + credentialResponse = await credentialRequestClient.acquireCredentialsUsingProof({ + proofInput, + credentialTypes: offeredCredential.type, + format: offeredCredential.credentialSupported.format, + }) + } + + const credential = await this.handleCredentialResponse(agentContext, credentialResponse, { + verifyCredentialStatus: options.verifyCredentialStatus, + }) + + // Create credential record, but we don't store it yet (only after the user has accepted the credential) + const credentialRecord = new W3cCredentialRecord({ + credential, + tags: { + expandedTypes: [], + }, + }) + this.logger.debug('Full credential', credentialRecord) + + if (!isInlineCredentialOffer(offeredCredential)) { + const issuerMetadata = client.endpointMetadata.credentialIssuerMetadata + if (!issuerMetadata) { + // TODO: this should not happen + throw new AriesFrameworkError('Issuer metadata not found') + } + const supportedCredentialMetadata = offeredCredential.credentialSupported + // Set the OpenId4Vc credential metadata and update record + setOpenId4VcCredentialMetadata(credentialRecord, supportedCredentialMetadata, serverMetadata, issuerMetadata) + } + + receivedCredentials.push(credentialRecord) + } + + return receivedCredentials + } + + /** + * Get the options for the credential request. Internally this will resolve the proof of possession + * requirements, and based on that it will call the proofOfPossessionVerificationMethodResolver to + * allow the caller to select the correct verification method based on the requirements for the proof + * of possession. + */ + private async getCredentialRequestOptions( + agentContext: AgentContext, + options: { + proofOfPossessionVerificationMethodResolver: ProofOfPossessionVerificationMethodResolver + allowedCredentialFormats: SupportedCredentialFormats[] + allowedProofOfPossessionSignatureAlgorithms: JwaSignatureAlgorithm[] + offeredCredentialWithMetadata: OfferedCredentialsWithMetadata + } + ) { + const { signatureAlgorithm, supportedDidMethods, supportsAllDidMethods } = this.getProofOfPossessionRequirements( + agentContext, + { + offeredCredentialWithMetadata: options.offeredCredentialWithMetadata, + allowedCredentialFormats: options.allowedCredentialFormats, + allowedProofOfPossessionSignatureAlgorithms: options.allowedProofOfPossessionSignatureAlgorithms, + } + ) + + const JwkClass = getJwkClassFromJwaSignatureAlgorithm(signatureAlgorithm) + + if (!JwkClass) { + throw new AriesFrameworkError( + `Could not determine JWK key type based on JWA signature algorithm '${signatureAlgorithm}'` + ) + } + + const supportedVerificationMethods = getSupportedVerificationMethodTypesFromKeyType(JwkClass.keyType) + + const format = isInlineCredentialOffer(options.offeredCredentialWithMetadata) + ? options.offeredCredentialWithMetadata.inlineCredentialOffer.format + : options.offeredCredentialWithMetadata.credentialSupported.format + + // Now we need to determine the did method and alg based on the cryptographic suite + const verificationMethod = await options.proofOfPossessionVerificationMethodResolver({ + credentialFormat: format as SupportedCredentialFormats, + proofOfPossessionSignatureAlgorithm: signatureAlgorithm, + supportedVerificationMethods, + keyType: JwkClass.keyType, + supportedCredentialId: !isInlineCredentialOffer(options.offeredCredentialWithMetadata) + ? options.offeredCredentialWithMetadata.credentialSupported.id + : undefined, + supportsAllDidMethods, + supportedDidMethods, + }) + + // Make sure the verification method uses a supported did method + if ( + !supportsAllDidMethods && + // If supportedDidMethods is undefined, it means the issuer didn't include the binding methods in the metadata + // The user can still select a verification method, but we can't validate it + supportedDidMethods !== undefined && + !supportedDidMethods.find((supportedDidMethod) => verificationMethod.id.startsWith(supportedDidMethod)) + ) { + const { method } = parseDid(verificationMethod.id) + const supportedDidMethodsString = supportedDidMethods.join(', ') + throw new AriesFrameworkError( + `Verification method uses did method '${method}', but issuer only supports '${supportedDidMethodsString}'` + ) + } + + // Make sure the verification method uses a supported verification method type + if (!supportedVerificationMethods.includes(verificationMethod.type)) { + const supportedVerificationMethodsString = supportedVerificationMethods.join(', ') + throw new AriesFrameworkError( + `Verification method uses verification method type '${verificationMethod.type}', but only '${supportedVerificationMethodsString}' verification methods are supported for key type '${JwkClass.keyType}'` + ) + } + + return { verificationMethod, signatureAlgorithm } + } + + // TODO: i cannot view this + // todo https://sphereon.atlassian.net/browse/VDX-184 + /** + * Returns all entries from the credential offer. This includes both 'id' entries that reference a supported credential in the issuer metadata, + * as well as inline credential offers that do not reference a supported credential in the issuer metadata. + */ + private getOfferedCredentials( + credentialOfferRequestWithBaseUrl: CredentialOfferRequestWithBaseUrl + ): Array { + if (credentialOfferRequestWithBaseUrl.version < OpenId4VCIVersion.VER_1_0_11) { + const credentialOffer = + credentialOfferRequestWithBaseUrl.original_credential_offer as CredentialOfferPayloadV1_0_08 + + return typeof credentialOffer.credential_type === 'string' + ? [credentialOffer.credential_type] + : credentialOffer.credential_type + } else { + return credentialOfferRequestWithBaseUrl.credential_offer.credentials + } + } + + /** + * Return a normalized version of the credentials supported by the issuer. Can optionally filter based on the credentials + * that were offered, or the type of credentials that are supported. + * + * + * NOTE: for v1_0-08, a single credential id in the issuer metadata could have multiple formats. When retrieving the + * supported credentials, for v1_0-08, the format is appended to the id if there are multiple formats supported for + * that credential id. E.g. if the issuer metadata for v1_0-08 contains an entry with key `OpenBadgeCredential` and + * the supported formats are `jwt_vc-jsonld` and `ldp_vc`, then the id in the credentials supported will be + * `OpenBadgeCredential-jwt_vc-jsonld` and `OpenBadgeCredential-ldp_vc`, even though the offered credential is simply + * `OpenBadgeCredential`. + * + * NOTE: this method only returns the credentials supported by the issuer metadata. It does not take into account the inline + * credentials offered. Use {@link getOfferedCredentialsWithMetadata} to get both the inline and referenced offered credentials. + */ + private getCredentialsSupported( + client: OpenID4VCIClient, + restrictToOfferIds: boolean, + credentialSupportedId?: string + ): CredentialSupported[] { + const offeredIds = this.getOfferedCredentials(client.credentialOffer).filter( + (c): c is string => typeof c === 'string' + ) + + const credentialSupportedIds = restrictToOfferIds ? offeredIds : undefined + + const credentialsSupported = getSupportedCredentials({ + issuerMetadata: client.endpointMetadata.credentialIssuerMetadata, + version: client.version(), + credentialSupportedIds, + }) + + return credentialSupportedId + ? credentialsSupported.filter( + (credentialSupported) => + credentialSupported.id === credentialSupportedId || + credentialSupported.id === `${credentialSupportedId}-${credentialSupported.format}` + ) + : credentialsSupported + } + + /** + * Returns all entries from the credential offer with the associated metadata resolved. For inline entries, the offered credential object + * is included directly. For 'id' entries, the associated `credentials_supported` object is resolved from the issuer metadata. + * + * NOTE: for v1_0-08, a single credential id in the issuer metadata could have multiple formats. This means that the returned value + * from this method could contain multiple entries for a single credential id, but with different formats. This is detectable as the + * id will be the `-`. + */ + private getOfferedCredentialsWithMetadata = (client: OpenID4VCIClient) => { + const offeredCredentials: Array = [] + + for (const offeredCredential of this.getOfferedCredentials(client.credentialOffer)) { + // If the offeredCredential is a string, it references a supported credential in the issuer metadata + if (typeof offeredCredential === 'string') { + const credentialsSupported = this.getCredentialsSupported(client, false, offeredCredential) + + // Make sure the issuer metadata includes the offered credential. + if (credentialsSupported.length === 0) { + throw new Error( + `Offered credential '${offeredCredential}' is not present in the credentials_supported of the issuer metadata` + ) + } + + offeredCredentials.push( + ...credentialsSupported.map((credentialSupported) => { + return { credentialSupported, type: OfferedCredentialType.CredentialSupported } as const + }) + ) + } + // Otherwise it's an inline credential offer that does not reference a supported credential in the issuer metadata + else { + // TODO: we could transform the inline offer to the `CredentialSupported` format, but we'll only be able to populate + // the `format`, `types` and `@context` fields. It's not really clear how to determine the supported did methods, + // signature suites, etc.. for these inline credentials. + // We should also add a property to indicate to the user that this is an inline credential offer. + // if (offeredCredential.format === 'jwt_vc_json') { + // const supported = { + // format: offeredCredential.format, + // types: offeredCredential.types, + // } satisfies CredentialSupportedJwtVcJson; + // } else if (offeredCredential.format === 'jwt_vc_json-ld' || offeredCredential.format === 'ldp_vc') { + // const supported = { + // format: offeredCredential.format, + // '@context': offeredCredential.credential_definition['@context'], + // types: offeredCredential.credential_definition.types, + // } satisfies CredentialSupported; + // } + offeredCredentials.push({ + inlineCredentialOffer: offeredCredential, + type: OfferedCredentialType.InlineCredentialOffer, + } as const) + } + } + + return offeredCredentials + } + + /** + * Get the requirements for creating the proof of possession. Based on the allowed + * credential formats, the allowed proof of possession signature algorithms, and the + * credential type, this method will select the best credential format and signature + * algorithm to use, based on the order of preference. + */ + private getProofOfPossessionRequirements( + agentContext: AgentContext, + options: { + allowedCredentialFormats: SupportedCredentialFormats[] + offeredCredentialWithMetadata: OfferedCredentialsWithMetadata + allowedProofOfPossessionSignatureAlgorithms: JwaSignatureAlgorithm[] + } + ): ProofOfPossessionRequirements { + const { offeredCredentialWithMetadata, allowedCredentialFormats } = options + + // Extract format from offer + let format = + offeredCredentialWithMetadata.type === OfferedCredentialType.InlineCredentialOffer + ? offeredCredentialWithMetadata.inlineCredentialOffer.format + : offeredCredentialWithMetadata.credentialSupported.format + + // Get uniform format, so we don't have to deal with the different spec versions + format = getUniformFormat(format) + + const credentialMetadata = + offeredCredentialWithMetadata.type === OfferedCredentialType.CredentialSupported + ? offeredCredentialWithMetadata.credentialSupported + : undefined + + const issuerSupportedCryptographicSuites = credentialMetadata?.cryptographic_suites_supported + const issuerSupportedBindingMethods = + credentialMetadata?.cryptographic_binding_methods_supported ?? + // FIXME: somehow the MATTR Launchpad returns binding_methods_supported instead of cryptographic_binding_methods_supported + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + (credentialMetadata?.binding_methods_supported as string[] | undefined) + + if (!isInlineCredentialOffer(offeredCredentialWithMetadata)) { + const credentialMetadata = offeredCredentialWithMetadata.credentialSupported + if (!allowedCredentialFormats.includes(format as SupportedCredentialFormats)) { + throw new AriesFrameworkError( + `Issuer only supports format '${format}' for credential type '${ + credentialMetadata.id as string + }', but the wallet only allows formats '${options.allowedCredentialFormats.join(', ')}'` + ) + } + } + + // For each of the supported algs, find the key types, then find the proof types + const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) + + let potentialSignatureAlgorithm: JwaSignatureAlgorithm | undefined + + switch (format) { + case 'jwt_vc_json': + case 'jwt_vc_json-ld': + // If undefined, it means the issuer didn't include the cryptographic suites in the metadata + // We just guess that the first one is supported + if (issuerSupportedCryptographicSuites === undefined) { + potentialSignatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms[0] + } else { + potentialSignatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms.find((signatureAlgorithm) => + issuerSupportedCryptographicSuites.includes(signatureAlgorithm) + ) + } + break + case 'ldp_vc': + // If undefined, it means the issuer didn't include the cryptographic suites in the metadata + // We just guess that the first one is supported + if (issuerSupportedCryptographicSuites === undefined) { + potentialSignatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms[0] + } else { + // We need to find it based on the JSON-LD proof type + potentialSignatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms.find( + (signatureAlgorithm) => { + const JwkClass = getJwkClassFromJwaSignatureAlgorithm(signatureAlgorithm) + if (!JwkClass) return false + + // TODO: getByKeyType should return a list + const matchingSuite = signatureSuiteRegistry.getByKeyType(JwkClass.keyType) + if (!matchingSuite) return false + + return issuerSupportedCryptographicSuites.includes(matchingSuite.proofType) + } + ) + } + break + default: + throw new AriesFrameworkError( + `Unsupported requested credential format '${format}' with id ${ + credentialMetadata?.id ?? 'Inline credential offer' + }` + ) + } + + const supportsAllDidMethods = issuerSupportedBindingMethods?.includes('did') ?? false + const supportedDidMethods = issuerSupportedBindingMethods?.filter((method) => method.startsWith('did:')) + + if (!potentialSignatureAlgorithm) { + throw new AriesFrameworkError( + `Could not establish signature algorithm for format ${format} and id ${ + credentialMetadata?.id ?? 'Inline credential offer' + }` + ) + } + + return { + signatureAlgorithm: potentialSignatureAlgorithm, + supportedDidMethods, + supportsAllDidMethods, + } + } + + /** + * Returns the JWA Signature Algorithms that are supported by the wallet. + * + * This is an approximation based on the supported key types of the wallet. + * This is not 100% correct as a supporting a key type does not mean you support + * all the algorithms for that key type. However, this needs refactoring of the wallet + * that is planned for the 0.5.0 release. + */ + private getSupportedJwaSignatureAlgorithms(agentContext: AgentContext): JwaSignatureAlgorithm[] { + const supportedKeyTypes = agentContext.wallet.supportedKeyTypes + + // Extract the supported JWS algs based on the key types the wallet support. + const supportedJwaSignatureAlgorithms = supportedKeyTypes + // Map the supported key types to the supported JWK class + .map(getJwkClassFromKeyType) + // Filter out the undefined values + .filter((jwkClass): jwkClass is Exclude => jwkClass !== undefined) + // Extract the supported JWA signature algorithms from the JWK class + .map((jwkClass) => jwkClass.supportedSignatureAlgorithms) + // Flatten the array of arrays + .reduce((allAlgorithms, algorithms) => [...allAlgorithms, ...algorithms], []) + + return supportedJwaSignatureAlgorithms + } + + private async handleCredentialResponse( + agentContext: AgentContext, + credentialResponse: OpenIDResponse, + options: { verifyCredentialStatus: boolean } + ) { + this.logger.debug('Credential request response', credentialResponse) + + if (!credentialResponse.successBody) { + throw new AriesFrameworkError('Did not receive a successful credential response') + } + + const format = getUniformFormat(credentialResponse.successBody.format) + const difClaimFormat = fromOpenIdCredentialFormatProfileToDifClaimFormat(format as OpenIdCredentialFormatProfile) + + let credential: W3cVerifiableCredential + let result: W3cVerifyCredentialResult + if (difClaimFormat === ClaimFormat.LdpVc) { + credential = JsonTransformer.fromJSON(credentialResponse.successBody.credential, W3cJsonLdVerifiableCredential) + result = await this.w3cCredentialService.verifyCredential(agentContext, { + credential, + verifyCredentialStatus: options.verifyCredentialStatus, + }) + } else if (difClaimFormat === ClaimFormat.JwtVc) { + credential = W3cJwtVerifiableCredential.fromSerializedJwt(credentialResponse.successBody.credential as string) + result = await this.w3cCredentialService.verifyCredential(agentContext, { + credential, + verifyCredentialStatus: options.verifyCredentialStatus, + }) + } else { + throw new AriesFrameworkError(`Unsupported credential format ${credentialResponse.successBody.format}`) + } + + if (!result || !result.isValid) { + agentContext.config.logger.error('Failed to validate credential', { + result, + }) + throw new AriesFrameworkError(`Failed to validate credential, error = ${result.error?.message ?? 'Unknown'}`) + } + + return credential + } + + private signCallback(agentContext: AgentContext, verificationMethod: VerificationMethod) { + return async (jwt: Jwt, kid?: string) => { + if (!jwt.header) { + throw new AriesFrameworkError('No header present on JWT') + } + + if (!jwt.payload) { + throw new AriesFrameworkError('No payload present on JWT') + } + + if (!kid) { + throw new AriesFrameworkError('No KID is present in the callback') + } + + // We have determined the verification method before and already passed that when creating the callback, + // however we just want to make sure that the kid matches the verification method id + if (verificationMethod.id !== kid) { + throw new AriesFrameworkError(`kid ${kid} does not match verification method id ${verificationMethod.id}`) + } + + const key = getKeyFromVerificationMethod(verificationMethod) + const jwk = getJwkFromKey(key) + + const payload = JsonEncoder.toBuffer(jwt.payload) + if (!jwk.supportsSignatureAlgorithm(jwt.header.alg)) { + throw new AriesFrameworkError( + `kid ${kid} refers to a key of type '${jwk.keyType}', which does not support the JWS signature alg '${jwt.header.alg}'` + ) + } + + // We don't support these properties, remove them, so we can pass all other header properties to the JWS service + if (jwt.header.x5c || jwt.header.jwk) throw new AriesFrameworkError('x5c and jwk are not supported') + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { x5c: _x5c, jwk: _jwk, ...supportedHeaderOptions } = jwt.header + + const jws = await this.jwsService.createJwsCompact(agentContext, { + key, + payload, + protectedHeaderOptions: supportedHeaderOptions, + }) + + return jws + } + } +} + +function isInlineCredentialOffer(offeredCredential: OfferedCredentialsWithMetadata): offeredCredential is { + inlineCredentialOffer: CredentialOfferFormat + type: OfferedCredentialType.InlineCredentialOffer +} { + return offeredCredential.type === OfferedCredentialType.InlineCredentialOffer +} diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts new file mode 100644 index 0000000000..5c62a2145b --- /dev/null +++ b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts @@ -0,0 +1,185 @@ +import type { JwaSignatureAlgorithm, KeyType, VerificationMethod } from '@aries-framework/core' + +import { OpenIdCredentialFormatProfile } from './utils/claimFormatMapping' + +/** + * The credential formats that are supported by the openid4vc holder + */ +export type SupportedCredentialFormats = OpenIdCredentialFormatProfile.JwtVcJson | OpenIdCredentialFormatProfile.LdpVc + +export const supportedCredentialFormats = [ + OpenIdCredentialFormatProfile.JwtVcJson, + OpenIdCredentialFormatProfile.LdpVc, +] satisfies OpenIdCredentialFormatProfile[] + +/** + * Options that are used for the pre-authorized code flow. + */ +export interface PreAuthCodeFlowOptions { + issuerUri: string + verifyCredentialStatus: boolean + + /** + * A list of allowed credential formats in order of preference. + * + * If the issuer supports one of the allowed formats, that first format that is supported + * from the list will be used. + * + * If the issuer doesn't support any of the allowed formats, an error is thrown + * and the request is aborted. + */ + allowedCredentialFormats?: SupportedCredentialFormats[] + + /** + * A list of allowed proof of possession signature algorithms in order of preference. + * + * Note that the signature algorithms must be supported by the wallet implementation. + * Signature algorithms that are not supported by the wallet will be ignored. + * + * The proof of possession (pop) signature algorithm is used in the credential request + * to bind the credential to a did. In most cases the JWA signature algorithm + * that is used in the pop will determine the cryptographic suite that is used + * for signing the credential, but this not a requirement for the spec. E.g. if the + * pop uses EdDsa, the credential will most commonly also use EdDsa, or Ed25519Signature2018/2020. + */ + allowedProofOfPossessionSignatureAlgorithms?: JwaSignatureAlgorithm[] + + /** + * A function that should resolve a verification method based on the options passed. + * This method will be called once for each of the credentials that are included + * in the credential offer. + * + * Based on the credential format, JWA signature algorithm, verification method types + * and did methods, the resolver must return a verification method that will be used + * for the proof of possession signature. + */ + proofOfPossessionVerificationMethodResolver: ProofOfPossessionVerificationMethodResolver +} + +/** + * Options that are used for the authorization code flow. + * Extends the pre-authorized code flow options. + */ +export interface AuthCodeFlowOptions extends PreAuthCodeFlowOptions { + clientId: string + authorizationCode: string + codeVerifier: string + redirectUri: string +} + +/** + * The options that are used to generate the authorization url. + * + * NOTE: The `code_challenge` property is omitted here + * because we assume it will always be SHA256 + * as clear text code challenges are unsafe. + */ +export interface GenerateAuthorizationUrlOptions { + initiationUri: string + clientId: string + redirectUri: string + scope?: string[] +} + +export interface ProofOfPossessionVerificationMethodResolverOptions { + /** + * The credential format that will be requested from the issuer. + * E.g. `jwt_vc` or `ldp_vc`. + */ + credentialFormat: SupportedCredentialFormats + + /** + * The JWA Signature Algorithm that will be used in the proof of possession. + * This is based on the `allowedProofOfPossessionSignatureAlgorithms` passed + * to the request credential method, and the supported signature algorithms. + */ + proofOfPossessionSignatureAlgorithm: JwaSignatureAlgorithm + + /** + * This is a list of verification methods types that are supported + * for creating the proof of possession signature. The returned + * verification method type must be of one of these types. + */ + supportedVerificationMethods: string[] + + /** + * The key type that will be used to create the proof of possession signature. + * This is related to the verification method and the signature algorithm, and + * is added for convenience. + */ + keyType: KeyType + + /** + * The credential type that will be requested from the issuer. This is + * based on the credential types that are included the credential offer. + * + * If the offered credential is an inline credential offer, the value + * will be `undefined`. + */ + // TODO: do we need credentialType here? + supportedCredentialId?: string + + /** + * Whether the issuer supports the `did` cryptographic binding method, + * indicating they support all did methods. In most cases, they do not + * support all did methods, and it means we have to make an assumption + * about the did methods they support. + * + * If this value is `false`, the `supportedDidMethods` property will + * contain a list of supported did methods. + */ + supportsAllDidMethods: boolean + + /** + * A list of supported did methods. This is only used if the `supportsAllDidMethods` + * property is `false`. When this array is populated, the returned verification method + * MUST be based on one of these did methods. + * + * The did methods are returned in the format `did:`, e.g. `did:web`. + * + * The value is undefined in the case the supported did methods could not be extracted. + * This is the case when an inline credential was used, or when the issuer didn't include + * the supported did methods in the issuer metadata. + * + * NOTE: an empty array (no did methods supported) has a different meaning from the value + * being undefined (the supported did methods could not be extracted). If `supportsAllDidMethods` + * is true, the value of this property MUST be ignored. + */ + supportedDidMethods?: string[] +} + +/** + * The proof of possession verification method resolver is a function that can be passed by the + * user of the framework and allows them to determine which verification method should be used + * for the proof of possession signature. + */ +export type ProofOfPossessionVerificationMethodResolver = ( + options: ProofOfPossessionVerificationMethodResolverOptions +) => Promise | VerificationMethod + +/** + * @internal + */ +export interface ProofOfPossessionRequirements { + signatureAlgorithm: JwaSignatureAlgorithm + supportedDidMethods?: string[] + supportsAllDidMethods: boolean +} + +/** + * @internal + */ +export enum AuthFlowType { + AuthorizationCodeFlow, + PreAuthorizedCodeFlow, +} + +type WithFlowType = Options & { flowType: FlowType } + +/** + * The options that are used to request a credential from an issuer. + * @internal + */ +export type RequestCredentialOptions = + | WithFlowType + | WithFlowType diff --git a/packages/openid4vc-holder/src/index.ts b/packages/openid4vc-holder/src/index.ts new file mode 100644 index 0000000000..c81821681c --- /dev/null +++ b/packages/openid4vc-holder/src/index.ts @@ -0,0 +1,21 @@ +import 'fast-text-encoding' + +export * from './OpenId4VcHolderApi' +export * from './OpenId4VcHolderModule' +export * from './OpenId4VcHolderService' +// Contains internal types, so we don't export everything +export { + AuthCodeFlowOptions, + GenerateAuthorizationUrlOptions, + PreAuthCodeFlowOptions, + ProofOfPossessionVerificationMethodResolver, + ProofOfPossessionVerificationMethodResolverOptions, + RequestCredentialOptions, + SupportedCredentialFormats, +} from './OpenId4VcHolderServiceOptions' +export { + getOpenId4VcCredentialMetadata, + OpenId4VcCredentialMetadata, + OpenIdCredentialFormatProfile, + setOpenId4VcCredentialMetadata, +} from './utils' diff --git a/packages/openid4vc-holder/src/utils/Formats.ts b/packages/openid4vc-holder/src/utils/Formats.ts new file mode 100644 index 0000000000..7d13fa6ef9 --- /dev/null +++ b/packages/openid4vc-holder/src/utils/Formats.ts @@ -0,0 +1,41 @@ +import type { OID4VCICredentialFormat } from '@sphereon/oid4vci-common' +import type { CredentialFormat } from '@sphereon/ssi-types' + +import { OpenId4VCIVersion } from '@sphereon/oid4vci-common' + +// Base on https://github.com/Sphereon-Opensource/OID4VCI/pull/54/files + +const isUniformFormat = (format: string): format is OID4VCICredentialFormat => { + return ['jwt_vc_json', 'jwt_vc_json-ld', 'ldp_vc'].includes(format) +} + +export function getUniformFormat(format: string | OID4VCICredentialFormat | CredentialFormat): OID4VCICredentialFormat { + // Already valid format + if (isUniformFormat(format)) { + return format + } + + // Older formats + if (format === 'jwt_vc' || format === 'jwt') { + return 'jwt_vc_json' + } + if (format === 'ldp_vc' || format === 'ldp') { + return 'ldp_vc' + } + + throw new Error(`Invalid format: ${format}`) +} + +export function getFormatForVersion(format: string, version: OpenId4VCIVersion) { + const uniformFormat = isUniformFormat(format) ? format : getUniformFormat(format) + + if (version < OpenId4VCIVersion.VER_1_0_11) { + if (uniformFormat === 'jwt_vc_json') { + return 'jwt_vc' as const + } else if (uniformFormat === 'ldp_vc' || uniformFormat === 'jwt_vc_json-ld') { + return 'ldp_vc' as const + } + } + + return uniformFormat +} diff --git a/packages/openid4vc-holder/src/utils/IssuerMetadataUtils.ts b/packages/openid4vc-holder/src/utils/IssuerMetadataUtils.ts new file mode 100644 index 0000000000..827cfdaa5e --- /dev/null +++ b/packages/openid4vc-holder/src/utils/IssuerMetadataUtils.ts @@ -0,0 +1,113 @@ +import type { + CredentialIssuerMetadata, + CredentialSupported, + CredentialSupportedTypeV1_0_08, + CredentialSupportedV1_0_08, + IssuerMetadataV1_0_08, + MetadataDisplay, +} from '@sphereon/oid4vci-common' + +import { OpenId4VCIVersion } from '@sphereon/oid4vci-common' + +export function getSupportedCredentials(opts?: { + issuerMetadata?: CredentialIssuerMetadata | IssuerMetadataV1_0_08 + version: OpenId4VCIVersion + credentialSupportedIds?: string[] +}): CredentialSupported[] { + const { issuerMetadata } = opts ?? {} + let credentialsSupported: CredentialSupported[] + if (!issuerMetadata) { + return [] + } + const { version, credentialSupportedIds } = opts ?? { version: OpenId4VCIVersion.VER_1_0_11 } + + const usesTransformedCredentialsSupported = + version === OpenId4VCIVersion.VER_1_0_08 || !Array.isArray(issuerMetadata.credentials_supported) + if (usesTransformedCredentialsSupported) { + credentialsSupported = credentialsSupportedV8ToV11((issuerMetadata as IssuerMetadataV1_0_08).credentials_supported) + } else { + credentialsSupported = (issuerMetadata as CredentialIssuerMetadata).credentials_supported + } + + if (credentialsSupported === undefined || credentialsSupported.length === 0) { + return [] + } else if (!credentialSupportedIds || credentialSupportedIds.length === 0) { + return credentialsSupported + } + + const credentialSupportedOverlap: CredentialSupported[] = [] + for (const credentialSupportedId of credentialSupportedIds) { + if (typeof credentialSupportedId === 'string') { + const supported = credentialsSupported.find((sup) => { + // Match id to offerType + if (sup.id === credentialSupportedId) return true + + // If the credential was transformed and the v8 variant supported multiple formats for the id, we + // check if there is an id with the format + // see credentialsSupportedV8ToV11 + if (usesTransformedCredentialsSupported && sup.id === `${credentialSupportedId}-${sup.format}`) return true + + return false + }) + if (supported) { + credentialSupportedOverlap.push(supported) + } + } + } + + return credentialSupportedOverlap +} + +export function credentialsSupportedV8ToV11(supportedV8: CredentialSupportedTypeV1_0_08): CredentialSupported[] { + return Object.entries(supportedV8).flatMap((entry) => { + const type = entry[0] + const supportedV8 = entry[1] + return credentialSupportedV8ToV11(type, supportedV8) + }) +} + +export function credentialSupportedV8ToV11( + key: string, + supportedV8: CredentialSupportedV1_0_08 +): CredentialSupported[] { + const v8FormatEntries = Object.entries(supportedV8.formats) + + return v8FormatEntries.map((entry) => { + const format = entry[0] + const credentialSupportBrief = entry[1] + if (typeof format !== 'string') { + throw Error(`Unknown format received ${JSON.stringify(format)}`) + } + let credentialSupport: Partial = {} + + // v8 format included the credential type / id as the key of the object and it could contain multiple supported formats + // v11 format has an array where each entry only supports one format, and can only have an `id` property. We include the + // key from the v8 object as the id for the v11 object, but to prevent collisions (as multiple formats can be supported under + // one key), we append the format to the key IF there's more than one format supported under the key. + const id = v8FormatEntries.length > 1 ? `${key}-${format}` : key + + credentialSupport = { + format, + display: supportedV8.display, + ...credentialSupportBrief, + credentialSubject: supportedV8.claims, + id, + } + return credentialSupport as CredentialSupported + }) +} + +export function getIssuerDisplays( + metadata: CredentialIssuerMetadata | IssuerMetadataV1_0_08, + opts?: { prefLocales: string[] } +): MetadataDisplay[] { + const matchedDisplays = + metadata.display?.filter( + (item) => + !opts?.prefLocales || + opts.prefLocales.length === 0 || + (item.locale && opts.prefLocales.includes(item.locale)) || + !item.locale + ) ?? [] + return matchedDisplays.sort((item) => (item.locale ? opts?.prefLocales.indexOf(item.locale) ?? 1 : Number.MAX_VALUE)) +} diff --git a/packages/openid4vc-holder/src/utils/__tests__/claimFormatMapping.test.ts b/packages/openid4vc-holder/src/utils/__tests__/claimFormatMapping.test.ts new file mode 100644 index 0000000000..a8bcdd9633 --- /dev/null +++ b/packages/openid4vc-holder/src/utils/__tests__/claimFormatMapping.test.ts @@ -0,0 +1,45 @@ +import { AriesFrameworkError, ClaimFormat } from '@aries-framework/core' + +import { + fromDifClaimFormatToOpenIdCredentialFormatProfile, + fromOpenIdCredentialFormatProfileToDifClaimFormat, + OpenIdCredentialFormatProfile, +} from '../claimFormatMapping' + +describe('claimFormatMapping', () => { + it('should convert from openid credential format profile to DIF claim format', () => { + expect(fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.LdpVc)).toStrictEqual( + OpenIdCredentialFormatProfile.LdpVc + ) + + expect(fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.JwtVc)).toStrictEqual( + OpenIdCredentialFormatProfile.JwtVcJson + ) + + expect(() => fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.Jwt)).toThrow(AriesFrameworkError) + + expect(() => fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.Ldp)).toThrow(AriesFrameworkError) + + expect(() => fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.JwtVp)).toThrow(AriesFrameworkError) + + expect(() => fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.LdpVp)).toThrow(AriesFrameworkError) + }) + + it('should convert from DIF claim format to openid credential format profile', () => { + expect(fromOpenIdCredentialFormatProfileToDifClaimFormat(OpenIdCredentialFormatProfile.JwtVcJson)).toStrictEqual( + ClaimFormat.JwtVc + ) + + expect(fromOpenIdCredentialFormatProfileToDifClaimFormat(OpenIdCredentialFormatProfile.JwtVcJsonLd)).toStrictEqual( + ClaimFormat.JwtVc + ) + + expect(fromOpenIdCredentialFormatProfileToDifClaimFormat(OpenIdCredentialFormatProfile.LdpVc)).toStrictEqual( + ClaimFormat.LdpVc + ) + + expect(() => fromOpenIdCredentialFormatProfileToDifClaimFormat(OpenIdCredentialFormatProfile.MsoMdoc)).toThrow( + AriesFrameworkError + ) + }) +}) diff --git a/packages/openid4vc-holder/src/utils/claimFormatMapping.ts b/packages/openid4vc-holder/src/utils/claimFormatMapping.ts new file mode 100644 index 0000000000..3ab952f94f --- /dev/null +++ b/packages/openid4vc-holder/src/utils/claimFormatMapping.ts @@ -0,0 +1,40 @@ +import { AriesFrameworkError, ClaimFormat } from '@aries-framework/core' + +export enum OpenIdCredentialFormatProfile { + JwtVcJson = 'jwt_vc_json', + JwtVcJsonLd = 'jwt_vc_json-ld', + LdpVc = 'ldp_vc', + MsoMdoc = 'mso_mdoc', +} + +export const fromDifClaimFormatToOpenIdCredentialFormatProfile = ( + claimFormat: ClaimFormat +): OpenIdCredentialFormatProfile => { + switch (claimFormat) { + case ClaimFormat.JwtVc: + return OpenIdCredentialFormatProfile.JwtVcJson + case ClaimFormat.LdpVc: + return OpenIdCredentialFormatProfile.LdpVc + default: + throw new AriesFrameworkError( + `Unsupported DIF claim format, ${claimFormat}, to map to an openid credential format profile` + ) + } +} + +export const fromOpenIdCredentialFormatProfileToDifClaimFormat = ( + openidCredentialFormatProfile: OpenIdCredentialFormatProfile +): ClaimFormat => { + switch (openidCredentialFormatProfile) { + case OpenIdCredentialFormatProfile.JwtVcJson: + return ClaimFormat.JwtVc + case OpenIdCredentialFormatProfile.JwtVcJsonLd: + return ClaimFormat.JwtVc + case OpenIdCredentialFormatProfile.LdpVc: + return ClaimFormat.LdpVc + default: + throw new AriesFrameworkError( + `Unsupported openid credential format profile, ${openidCredentialFormatProfile}, to map to a DIF claim format` + ) + } +} diff --git a/packages/openid4vc-holder/src/utils/index.ts b/packages/openid4vc-holder/src/utils/index.ts new file mode 100644 index 0000000000..ee55f476ed --- /dev/null +++ b/packages/openid4vc-holder/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './claimFormatMapping' +export * from './metadata' diff --git a/packages/openid4vc-holder/src/utils/metadata.ts b/packages/openid4vc-holder/src/utils/metadata.ts new file mode 100644 index 0000000000..2ae316632b --- /dev/null +++ b/packages/openid4vc-holder/src/utils/metadata.ts @@ -0,0 +1,69 @@ +import type { W3cCredentialRecord } from '@aries-framework/core' +import type { + CredentialIssuerMetadata, + CredentialsSupportedDisplay, + CredentialSupported, + EndpointMetadata, + IssuerCredentialSubject, + IssuerMetadataV1_0_08, + MetadataDisplay, +} from '@sphereon/oid4vci-common' + +export interface OpenId4VcCredentialMetadata { + credential: { + display?: CredentialsSupportedDisplay[] + order?: string[] + credentialSubject: IssuerCredentialSubject + } + issuer: { + display?: MetadataDisplay[] + id: string + } +} + +// what does this mean +const openId4VcCredentialMetadataKey = '_paradym/openId4VcCredentialMetadata' + +function extractOpenId4VcCredentialMetadata( + credentialMetadata: CredentialSupported, + serverMetadata: EndpointMetadata, + serverMetadataResult: CredentialIssuerMetadata | IssuerMetadataV1_0_08 +) { + return { + credential: { + display: credentialMetadata.display, + order: credentialMetadata.order, + credentialSubject: credentialMetadata.credentialSubject, + }, + issuer: { + display: serverMetadataResult.credentialIssuerMetadata?.display, + id: serverMetadata.issuer, + }, + } +} + +/** + * Gets the OpenId4Vc credential metadata from the given W3C credential record. + */ +export function getOpenId4VcCredentialMetadata( + w3cCredentialRecord: W3cCredentialRecord +): OpenId4VcCredentialMetadata | null { + return w3cCredentialRecord.metadata.get(openId4VcCredentialMetadataKey) +} + +/** + * Sets the OpenId4Vc credential metadata on the given W3C credential record. + * + * NOTE: this does not save the record. + */ +export function setOpenId4VcCredentialMetadata( + w3cCredentialRecord: W3cCredentialRecord, + credentialMetadata: CredentialSupported, + serverMetadata: EndpointMetadata, + serverMetadataResult: CredentialIssuerMetadata | IssuerMetadataV1_0_08 +) { + w3cCredentialRecord.metadata.set( + openId4VcCredentialMetadataKey, + extractOpenId4VcCredentialMetadata(credentialMetadata, serverMetadata, serverMetadataResult) + ) +} diff --git a/packages/openid4vc-holder/tests/OpenId4VcClientModule.test.ts b/packages/openid4vc-holder/tests/OpenId4VcClientModule.test.ts new file mode 100644 index 0000000000..46d2bb7308 --- /dev/null +++ b/packages/openid4vc-holder/tests/OpenId4VcClientModule.test.ts @@ -0,0 +1,26 @@ +/* eslint-disable @typescript-eslint/unbound-method */ +import type { DependencyManager } from '@aries-framework/core' + +import { OpenId4VcHolderApi } from '../src/OpenId4VcHolderApi' +import { OpenId4VcHolderModule } from '../src/OpenId4VcHolderModule' +import { OpenId4VcHolderService } from '../src/OpenId4VcHolderService' + +const dependencyManager = { + registerInstance: jest.fn(), + registerSingleton: jest.fn(), + registerContextScoped: jest.fn(), + resolve: jest.fn().mockReturnValue({ logger: { warn: jest.fn() } }), +} as unknown as DependencyManager + +describe('OpenId4VcClientModule', () => { + test('registers dependencies on the dependency manager', () => { + const openId4VcClientModule = new OpenId4VcHolderModule() + openId4VcClientModule.register(dependencyManager) + + expect(dependencyManager.registerContextScoped).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerContextScoped).toHaveBeenCalledWith(OpenId4VcHolderApi) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VcHolderService) + }) +}) diff --git a/packages/openid4vc-holder/tests/fixtures.ts b/packages/openid4vc-holder/tests/fixtures.ts new file mode 100644 index 0000000000..54e46bb496 --- /dev/null +++ b/packages/openid4vc-holder/tests/fixtures.ts @@ -0,0 +1,460 @@ +export const mattrLaunchpadJsonLd_draft_08 = { + credentialOffer: + 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=OpenBadgeCredential&pre-authorized_code=krBcsBIlye2T-G4-rHHnRZUCah9uzDKwohJK6ABNvL-', + getMetadataResponse: { + authorization_endpoint: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/authorize', + token_endpoint: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/token', + jwks_uri: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/jwks', + token_endpoint_auth_methods_supported: [ + 'none', + 'client_secret_basic', + 'client_secret_jwt', + 'client_secret_post', + 'private_key_jwt', + ], + code_challenge_methods_supported: ['S256'], + grant_types_supported: ['authorization_code', 'urn:ietf:params:oauth:grant-type:pre-authorized_code'], + response_modes_supported: ['form_post', 'fragment', 'query'], + response_types_supported: ['code id_token', 'code', 'id_token', 'none'], + scopes_supported: ['OpenBadgeCredential', 'AcademicAward', 'LearnerProfile', 'PermanentResidentCard'], + token_endpoint_auth_signing_alg_values_supported: ['HS256', 'RS256', 'PS256', 'ES256', 'EdDSA'], + credential_endpoint: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/credential', + credentials_supported: { + OpenBadgeCredential: { + formats: { + ldp_vc: { + name: 'JFF x vc-edu PlugFest 2', + description: "MATTR's submission for JFF Plugfest 2", + types: ['OpenBadgeCredential'], + binding_methods_supported: ['did'], + cryptographic_suites_supported: ['Ed25519Signature2018'], + }, + }, + }, + AcademicAward: { + formats: { + ldp_vc: { + name: 'Example Academic Award', + description: 'Microcredential from the MyCreds Network.', + types: ['AcademicAward'], + binding_methods_supported: ['did'], + cryptographic_suites_supported: ['Ed25519Signature2018'], + }, + }, + }, + LearnerProfile: { + formats: { + ldp_vc: { + name: 'Digitary Learner Profile', + description: 'Example', + types: ['LearnerProfile'], + binding_methods_supported: ['did'], + cryptographic_suites_supported: ['Ed25519Signature2018'], + }, + }, + }, + PermanentResidentCard: { + formats: { + ldp_vc: { + name: 'Permanent Resident Card', + description: 'Government of Kakapo', + types: ['PermanentResidentCard'], + binding_methods_supported: ['did'], + cryptographic_suites_supported: ['Ed25519Signature2018'], + }, + }, + }, + }, + }, + + acquireAccessTokenResponse: { + access_token: '7nikUotMQefxn7oRX56R7MDNE7KJTGfwGjOkHzGaUIG', + expires_in: 3600, + scope: 'OpenBadgeCredential', + token_type: 'Bearer', + }, + credentialResponse: { + format: 'ldp_vc', + credential: { + type: ['VerifiableCredential', 'VerifiableCredentialExtension', 'OpenBadgeCredential'], + issuer: { + id: 'did:web:launchpad.vii.electron.mattrlabs.io', + name: 'Jobs for the Future (JFF)', + iconUrl: 'https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png', + image: 'https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png', + }, + name: 'JFF x vc-edu PlugFest 2', + description: "MATTR's submission for JFF Plugfest 2", + credentialBranding: { + backgroundColor: '#464c49', + }, + issuanceDate: '2023-01-25T16:58:06.292Z', + credentialSubject: { + id: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + type: ['AchievementSubject'], + achievement: { + id: 'urn:uuid:bd6d9316-f7ae-4073-a1e5-2f7f5bd22922', + name: 'JFF x vc-edu PlugFest 2 Interoperability', + type: ['Achievement'], + image: { + id: 'https://w3c-ccg.github.io/vc-ed/plugfest-2-2022/images/JFF-VC-EDU-PLUGFEST2-badge-image.png', + type: 'Image', + }, + criteria: { + type: 'Criteria', + narrative: + 'Solutions providers earned this badge by demonstrating interoperability between multiple providers based on the OBv3 candidate final standard, with some additional required fields. Credential issuers earning this badge successfully issued a credential into at least two wallets. Wallet implementers earning this badge successfully displayed credentials issued by at least two different credential issuers.', + }, + description: + 'This credential solution supports the use of OBv3 and w3c Verifiable Credentials and is interoperable with at least two other solutions. This was demonstrated successfully during JFF x vc-edu PlugFest 2.', + }, + }, + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + { + '@vocab': 'https://w3id.org/security/undefinedTerm#', + }, + 'https://mattr.global/contexts/vc-extensions/v1', + 'https://purl.imsglobal.org/spec/ob/v3p0/context.json', + 'https://w3c-ccg.github.io/vc-status-rl-2020/contexts/vc-revocation-list-2020/v1.jsonld', + ], + credentialStatus: { + id: 'https://launchpad.vii.electron.mattrlabs.io/core/v1/revocation-lists/b4aa46a0-5539-4a6b-aa03-8f6791c22ce3#49', + type: 'RevocationList2020Status', + revocationListIndex: '49', + revocationListCredential: + 'https://launchpad.vii.electron.mattrlabs.io/core/v1/revocation-lists/b4aa46a0-5539-4a6b-aa03-8f6791c22ce3', + }, + proof: { + type: 'Ed25519Signature2018', + created: '2023-01-25T16:58:07Z', + jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..PrpRKt60yXOzMNiQY5bELX40F6Svwm-FyQ-Jv02VJDfTTH8GPPByjtOb_n3YfWidQVgySfGQ_H7VmCGjvsU6Aw', + proofPurpose: 'assertionMethod', + verificationMethod: 'did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg', + }, + }, + }, +} + +export const waltIdJffJwt_draft_08 = { + credentialOffer: + 'openid-initiate-issuance://?issuer=https%3A%2F%2Fjff.walt.id%2Fissuer-api%2Fdefault%2Foidc%2F&credential_type=VerifiableId&pre-authorized_code=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI4YmI0NWZiNC0zNDc1LTQ5YzItODVjNy0wYjkxZjY4N2RhNDQiLCJwcmUtYXV0aG9yaXplZCI6dHJ1ZX0.R8nHseZJvU3uVL3Ox-97i1HUnvjZH6wKSWDO_i8D12I&user_pin_required=false', + getMetadataResponse: { + authorization_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/fulfillPAR', + token_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/token', + pushed_authorization_request_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/par', + issuer: 'https://jff.walt.id/issuer-api/default', + jwks_uri: 'https://jff.walt.id/issuer-api/default/oidc', + grant_types_supported: ['authorization_code', 'urn:ietf:params:oauth:grant-type:pre-authorized_code'], + request_uri_parameter_supported: true, + credentials_supported: { + VerifiableId: { + display: [{ name: 'VerifiableId' }], + formats: { + ldp_vc: { + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: [ + 'Ed25519Signature2018', + 'Ed25519Signature2020', + 'EcdsaSecp256k1Signature2019', + 'RsaSignature2018', + 'JsonWebSignature2020', + 'JcsEd25519Signature2020', + ], + types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableId'], + }, + jwt_vc: { + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], + types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableId'], + }, + }, + }, + VerifiableDiploma: { + display: [{ name: 'VerifiableDiploma' }], + formats: { + ldp_vc: { + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: [ + 'Ed25519Signature2018', + 'Ed25519Signature2020', + 'EcdsaSecp256k1Signature2019', + 'RsaSignature2018', + 'JsonWebSignature2020', + 'JcsEd25519Signature2020', + ], + types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableDiploma'], + }, + jwt_vc: { + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], + types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableDiploma'], + }, + }, + }, + VerifiableVaccinationCertificate: { + display: [{ name: 'VerifiableVaccinationCertificate' }], + formats: { + ldp_vc: { + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: [ + 'Ed25519Signature2018', + 'Ed25519Signature2020', + 'EcdsaSecp256k1Signature2019', + 'RsaSignature2018', + 'JsonWebSignature2020', + 'JcsEd25519Signature2020', + ], + types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableVaccinationCertificate'], + }, + jwt_vc: { + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], + types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableVaccinationCertificate'], + }, + }, + }, + ProofOfResidence: { + display: [{ name: 'ProofOfResidence' }], + formats: { + ldp_vc: { + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: [ + 'Ed25519Signature2018', + 'Ed25519Signature2020', + 'EcdsaSecp256k1Signature2019', + 'RsaSignature2018', + 'JsonWebSignature2020', + 'JcsEd25519Signature2020', + ], + types: ['VerifiableCredential', 'VerifiableAttestation', 'ProofOfResidence'], + }, + jwt_vc: { + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], + types: ['VerifiableCredential', 'VerifiableAttestation', 'ProofOfResidence'], + }, + }, + }, + ParticipantCredential: { + display: [{ name: 'ParticipantCredential' }], + formats: { + ldp_vc: { + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: [ + 'Ed25519Signature2018', + 'Ed25519Signature2020', + 'EcdsaSecp256k1Signature2019', + 'RsaSignature2018', + 'JsonWebSignature2020', + 'JcsEd25519Signature2020', + ], + types: ['VerifiableCredential', 'ParticipantCredential'], + }, + jwt_vc: { + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], + types: ['VerifiableCredential', 'ParticipantCredential'], + }, + }, + }, + Europass: { + display: [{ name: 'Europass' }], + formats: { + ldp_vc: { + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: [ + 'Ed25519Signature2018', + 'Ed25519Signature2020', + 'EcdsaSecp256k1Signature2019', + 'RsaSignature2018', + 'JsonWebSignature2020', + 'JcsEd25519Signature2020', + ], + types: ['VerifiableCredential', 'VerifiableAttestation', 'Europass'], + }, + jwt_vc: { + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], + types: ['VerifiableCredential', 'VerifiableAttestation', 'Europass'], + }, + }, + }, + OpenBadgeCredential: { + display: [{ name: 'OpenBadgeCredential' }], + formats: { + ldp_vc: { + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: [ + 'Ed25519Signature2018', + 'Ed25519Signature2020', + 'EcdsaSecp256k1Signature2019', + 'RsaSignature2018', + 'JsonWebSignature2020', + 'JcsEd25519Signature2020', + ], + types: ['VerifiableCredential', 'OpenBadgeCredential'], + }, + jwt_vc: { + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], + types: ['VerifiableCredential', 'OpenBadgeCredential'], + }, + }, + }, + }, + credential_issuer: { + display: [{ locale: null, name: 'https://jff.walt.id/issuer-api/default' }], + }, + credential_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/credential', + subject_types_supported: ['public'], + }, + + acquireAccessTokenResponse: { + access_token: '8bb45fb4-3475-49c2-85c7-0b91f687da44', + refresh_token: 'WEjORX8NZccRGtRN4yvXFdYE8MeAOaLLmmGlcRbutq4', + c_nonce: 'cbad6376-f882-44c5-ae88-19bccc0de124', + id_token: + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI4YmI0NWZiNC0zNDc1LTQ5YzItODVjNy0wYjkxZjY4N2RhNDQifQ.Mca0Ln1AvNlxBJftYc1PZKQBlGdBmrHsFRQSBDoCgD0', + token_type: 'Bearer', + expires_in: 300, + }, + + credentialResponse: { + credential: + 'eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCMwIiwidHlwIjoiSldUIiwiYWxnIjoiRWREU0EifQ.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCIsInN1YiI6ImRpZDprZXk6ekRuYWVpcFdnOURNWFB0OWpjbUFCcWFZUlZLYzE5dFgxeGZCUldGc0pTUG9VZE1udiIsIm5iZiI6MTY4NTM1MDc4OSwiaWF0IjoxNjg1MzUwNzg5LCJ2YyI6eyJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVmVyaWZpYWJsZUF0dGVzdGF0aW9uIiwiVmVyaWZpYWJsZUlkIl0sIkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sImlkIjoidXJuOnV1aWQ6NTljZTRhYzItZWM2NS00YjhmLThmOTYtZWE3ODUxMmRmOWQzIiwiaXNzdWVyIjoiZGlkOmp3azpleUpyZEhraU9pSlBTMUFpTENKMWMyVWlPaUp6YVdjaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWEybGtJam9pTjJRMlkySm1NalE0T1dJek5ESTNObUl4TnpJeE9UQTFORGxrTWpNNU1UZ2lMQ0o0SWpvaVJtNUZWVlZoZFdSdE9UbE9NekJpT0RCcWN6aFdkRFJCYms5NGRsSjNXSFJuVW1OTGNUTm5Ra2wxT0NJc0ltRnNaeUk2SWtWa1JGTkJJbjAiLCJpc3N1YW5jZURhdGUiOiIyMDIzLTA1LTI5VDA4OjU5OjQ5WiIsImlzc3VlZCI6IjIwMjMtMDUtMjlUMDg6NTk6NDlaIiwidmFsaWRGcm9tIjoiMjAyMy0wNS0yOVQwODo1OTo0OVoiLCJjcmVkZW50aWFsU2NoZW1hIjp7ImlkIjoiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3dhbHQtaWQvd2FsdGlkLXNzaWtpdC12Y2xpYi9tYXN0ZXIvc3JjL3Rlc3QvcmVzb3VyY2VzL3NjaGVtYXMvVmVyaWZpYWJsZUlkLmpzb24iLCJ0eXBlIjoiRnVsbEpzb25TY2hlbWFWYWxpZGF0b3IyMDIxIn0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6RG5hZWlwV2c5RE1YUHQ5amNtQUJxYVlSVktjMTl0WDF4ZkJSV0ZzSlNQb1VkTW52IiwiY3VycmVudEFkZHJlc3MiOlsiMSBCb3VsZXZhcmQgZGUgbGEgTGliZXJ0w6ksIDU5ODAwIExpbGxlIl0sImRhdGVPZkJpcnRoIjoiMTk5My0wNC0wOCIsImZhbWlseU5hbWUiOiJET0UiLCJmaXJzdE5hbWUiOiJKYW5lIiwiZ2VuZGVyIjoiRkVNQUxFIiwibmFtZUFuZEZhbWlseU5hbWVBdEJpcnRoIjoiSmFuZSBET0UiLCJwZXJzb25hbElkZW50aWZpZXIiOiIwOTA0MDA4MDg0SCIsInBsYWNlT2ZCaXJ0aCI6IkxJTExFLCBGUkFOQ0UifSwiZXZpZGVuY2UiOlt7ImRvY3VtZW50UHJlc2VuY2UiOlsiUGh5c2ljYWwiXSwiZXZpZGVuY2VEb2N1bWVudCI6WyJQYXNzcG9ydCJdLCJzdWJqZWN0UHJlc2VuY2UiOiJQaHlzaWNhbCIsInR5cGUiOlsiRG9jdW1lbnRWZXJpZmljYXRpb24iXSwidmVyaWZpZXIiOiJkaWQ6ZWJzaToyQTlCWjlTVWU2QmF0YWNTcHZzMVY1Q2RqSHZMcFE3YkVzaTJKYjZMZEhLblF4YU4ifV19LCJqdGkiOiJ1cm46dXVpZDo1OWNlNGFjMi1lYzY1LTRiOGYtOGY5Ni1lYTc4NTEyZGY5ZDMifQ.6Wn8X2tEQJ9CmX3-meCxDuGmevRdtivnjVkGPXzfnJ-1M6AU4SFxxon0JmMjdmO_h4P9sCEe9RTtyTJou2yeCA', + format: 'jwt_vc', + }, +} + +// This object is MANUALLY converted and should be updated when we have actual test vectors +export const waltIdJffJwt_draft_11 = { + credentialOffer: + 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fjff.walt.id%2Fissuer-api%2Fdefault%2Foidc%22%2C%22credentials%22%3A%5B%22VerifiableId%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22ABC%22%7D%7D%7D', + getMetadataResponse: { + authorization_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/fulfillPAR', + token_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/token', + pushed_authorization_request_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/par', + credential_issuer: 'https://jff.walt.id/issuer-api/default', + jwks_uri: 'https://jff.walt.id/issuer-api/default/oidc', + credential_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/credential', + subject_types_supported: ['public'], + grant_types_supported: ['authorization_code', 'urn:ietf:params:oauth:grant-type:pre-authorized_code'], + request_uri_parameter_supported: true, + credentials_supported: [ + { + id: 'VerifiableId', + format: 'jwt_vc_json', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], + types: ['VerifiableCredential', 'OpenBadgeCredential'], + }, + { + id: 'VerifiableDiploma', + display: [{ name: 'VerifiableDiploma' }], + format: 'ldp_vc', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: [ + 'Ed25519Signature2018', + 'Ed25519Signature2020', + 'EcdsaSecp256k1Signature2019', + 'RsaSignature2018', + 'JsonWebSignature2020', + 'JcsEd25519Signature2020', + ], + types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableDiploma'], + }, + { + id: 'VerifiableVaccinationCertificate', + display: [{ name: 'VerifiableVaccinationCertificate' }], + format: 'ldp_vc', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: [ + 'Ed25519Signature2018', + 'Ed25519Signature2020', + 'EcdsaSecp256k1Signature2019', + 'RsaSignature2018', + 'JsonWebSignature2020', + 'JcsEd25519Signature2020', + ], + types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableVaccinationCertificate'], + }, + { + id: 'ProofOfResidence', + display: [{ name: 'ProofOfResidence' }], + format: 'ldp_vc', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: [ + 'Ed25519Signature2018', + 'Ed25519Signature2020', + 'EcdsaSecp256k1Signature2019', + 'RsaSignature2018', + 'JsonWebSignature2020', + 'JcsEd25519Signature2020', + ], + types: ['VerifiableCredential', 'VerifiableAttestation', 'ProofOfResidence'], + }, + { + id: 'ParticipantCredential', + format: 'ldp_vc', + display: [{ name: 'ParticipantCredential' }], + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: [ + 'Ed25519Signature2018', + 'Ed25519Signature2020', + 'EcdsaSecp256k1Signature2019', + 'RsaSignature2018', + 'JsonWebSignature2020', + 'JcsEd25519Signature2020', + ], + types: ['VerifiableCredential', 'ParticipantCredential'], + }, + { + id: 'Europass', + display: [{ name: 'Europass' }], + format: 'ldp_vc', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: [ + 'Ed25519Signature2018', + 'Ed25519Signature2020', + 'EcdsaSecp256k1Signature2019', + 'RsaSignature2018', + 'JsonWebSignature2020', + 'JcsEd25519Signature2020', + ], + types: ['VerifiableCredential', 'VerifiableAttestation', 'Europass'], + }, + { + id: 'OpenBadgeCredential', + display: [{ name: 'OpenBadgeCredential' }], + format: 'ldp_vc', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: [ + 'Ed25519Signature2018', + 'Ed25519Signature2020', + 'EcdsaSecp256k1Signature2019', + 'RsaSignature2018', + 'JsonWebSignature2020', + 'JcsEd25519Signature2020', + ], + types: ['VerifiableCredential', 'OpenBadgeCredential'], + }, + ], + }, + + acquireAccessTokenResponse: { + access_token: '8bb45fb4-3475-49c2-85c7-0b91f687da44', + refresh_token: 'WEjORX8NZccRGtRN4yvXFdYE8MeAOaLLmmGlcRbutq4', + c_nonce: 'cbad6376-f882-44c5-ae88-19bccc0de124', + id_token: + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI4YmI0NWZiNC0zNDc1LTQ5YzItODVjNy0wYjkxZjY4N2RhNDQifQ.Mca0Ln1AvNlxBJftYc1PZKQBlGdBmrHsFRQSBDoCgD0', + token_type: 'Bearer', + expires_in: 300, + }, + + credentialResponse: { + credential: + 'eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCMwIiwidHlwIjoiSldUIiwiYWxnIjoiRWREU0EifQ.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCIsInN1YiI6ImRpZDprZXk6ekRuYWVpcFdnOURNWFB0OWpjbUFCcWFZUlZLYzE5dFgxeGZCUldGc0pTUG9VZE1udiIsIm5iZiI6MTY4NTM1MDc4OSwiaWF0IjoxNjg1MzUwNzg5LCJ2YyI6eyJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVmVyaWZpYWJsZUF0dGVzdGF0aW9uIiwiVmVyaWZpYWJsZUlkIl0sIkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sImlkIjoidXJuOnV1aWQ6NTljZTRhYzItZWM2NS00YjhmLThmOTYtZWE3ODUxMmRmOWQzIiwiaXNzdWVyIjoiZGlkOmp3azpleUpyZEhraU9pSlBTMUFpTENKMWMyVWlPaUp6YVdjaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWEybGtJam9pTjJRMlkySm1NalE0T1dJek5ESTNObUl4TnpJeE9UQTFORGxrTWpNNU1UZ2lMQ0o0SWpvaVJtNUZWVlZoZFdSdE9UbE9NekJpT0RCcWN6aFdkRFJCYms5NGRsSjNXSFJuVW1OTGNUTm5Ra2wxT0NJc0ltRnNaeUk2SWtWa1JGTkJJbjAiLCJpc3N1YW5jZURhdGUiOiIyMDIzLTA1LTI5VDA4OjU5OjQ5WiIsImlzc3VlZCI6IjIwMjMtMDUtMjlUMDg6NTk6NDlaIiwidmFsaWRGcm9tIjoiMjAyMy0wNS0yOVQwODo1OTo0OVoiLCJjcmVkZW50aWFsU2NoZW1hIjp7ImlkIjoiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3dhbHQtaWQvd2FsdGlkLXNzaWtpdC12Y2xpYi9tYXN0ZXIvc3JjL3Rlc3QvcmVzb3VyY2VzL3NjaGVtYXMvVmVyaWZpYWJsZUlkLmpzb24iLCJ0eXBlIjoiRnVsbEpzb25TY2hlbWFWYWxpZGF0b3IyMDIxIn0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6RG5hZWlwV2c5RE1YUHQ5amNtQUJxYVlSVktjMTl0WDF4ZkJSV0ZzSlNQb1VkTW52IiwiY3VycmVudEFkZHJlc3MiOlsiMSBCb3VsZXZhcmQgZGUgbGEgTGliZXJ0w6ksIDU5ODAwIExpbGxlIl0sImRhdGVPZkJpcnRoIjoiMTk5My0wNC0wOCIsImZhbWlseU5hbWUiOiJET0UiLCJmaXJzdE5hbWUiOiJKYW5lIiwiZ2VuZGVyIjoiRkVNQUxFIiwibmFtZUFuZEZhbWlseU5hbWVBdEJpcnRoIjoiSmFuZSBET0UiLCJwZXJzb25hbElkZW50aWZpZXIiOiIwOTA0MDA4MDg0SCIsInBsYWNlT2ZCaXJ0aCI6IkxJTExFLCBGUkFOQ0UifSwiZXZpZGVuY2UiOlt7ImRvY3VtZW50UHJlc2VuY2UiOlsiUGh5c2ljYWwiXSwiZXZpZGVuY2VEb2N1bWVudCI6WyJQYXNzcG9ydCJdLCJzdWJqZWN0UHJlc2VuY2UiOiJQaHlzaWNhbCIsInR5cGUiOlsiRG9jdW1lbnRWZXJpZmljYXRpb24iXSwidmVyaWZpZXIiOiJkaWQ6ZWJzaToyQTlCWjlTVWU2QmF0YWNTcHZzMVY1Q2RqSHZMcFE3YkVzaTJKYjZMZEhLblF4YU4ifV19LCJqdGkiOiJ1cm46dXVpZDo1OWNlNGFjMi1lYzY1LTRiOGYtOGY5Ni1lYTc4NTEyZGY5ZDMifQ.6Wn8X2tEQJ9CmX3-meCxDuGmevRdtivnjVkGPXzfnJ-1M6AU4SFxxon0JmMjdmO_h4P9sCEe9RTtyTJou2yeCA', + format: 'jwt_vc', + }, +} diff --git a/packages/openid4vc-holder/tests/openid4vc-client.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vc-client.e2e.test.ts new file mode 100644 index 0000000000..4ae0e33b13 --- /dev/null +++ b/packages/openid4vc-holder/tests/openid4vc-client.e2e.test.ts @@ -0,0 +1,483 @@ +import type { KeyDidCreateOptions } from '@aries-framework/core' + +import { AskarModule } from '@aries-framework/askar' +import { + JwaSignatureAlgorithm, + Agent, + KeyType, + TypedArrayEncoder, + W3cCredentialRecord, + DidKey, +} from '@aries-framework/core' +import { agentDependencies } from '@aries-framework/node' +import { ariesAskar } from '@hyperledger/aries-askar-nodejs' +import nock, { cleanAll, enableNetConnect } from 'nock' + +import { OpenId4VcHolderModule } from '../src' +import { OpenIdCredentialFormatProfile } from '../src/utils/claimFormatMapping' + +import { + mattrLaunchpadJsonLd_draft_08, + // FIXME: we need a custom document loader for this, which is only present in AFJ core + // mattrLaunchpadJsonLd_draft_08, + waltIdJffJwt_draft_08, + waltIdJffJwt_draft_11, +} from './fixtures' + +const modules = { + openId4VcHolder: new OpenId4VcHolderModule(), + askar: new AskarModule({ + ariesAskar, + }), +} + +describe('OpenId4VcHolder', () => { + let agent: Agent + + beforeEach(async () => { + agent = new Agent({ + config: { + label: 'OpenId4VcHolder Test', + walletConfig: { + id: 'openid4vc-holder-test', + key: 'openid4vc-holder-test', + }, + }, + dependencies: agentDependencies, + modules, + }) + + await agent.initialize() + }) + + afterEach(async () => { + await agent.shutdown() + await agent.wallet.delete() + }) + + describe('[DRAFT 08]: Pre-authorized flow', () => { + afterEach(() => { + cleanAll() + enableNetConnect() + }) + + xit('[DRAFT 08]: Should successfully execute the pre-authorized flow using a did:key Ed25519 subject and JSON-LD credential', async () => { + const fixture = mattrLaunchpadJsonLd_draft_08 + /** + * Below we're setting up some mock HTTP responses. + * These responses are based on the openid-initiate-issuance URI above + * */ + + // setup temporary redirect mock + nock('https://launchpad.mattrlabs.com') + .get('/.well-known/openid-credential-issuer') + .reply(307, undefined, { + Location: 'https://launchpad.vii.electron.mattrlabs.io/.well-known/openid-credential-issuer', + }) + + .get('/.well-known/openid-configuration') + .reply(404) + + .get('/.well-known/oauth-authorization-server') + .reply(404) + + // setup server metadata response + nock('https://launchpad.vii.electron.mattrlabs.io') + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.getMetadataResponse) + + // setup access token response + .post('/oidc/v1/auth/token') + .reply(200, fixture.acquireAccessTokenResponse) + + // setup credential request response + .post('/oidc/v1/auth/credential') + .reply(200, fixture.credentialResponse) + + const did = await agent.dids.create({ + method: 'key', + options: { + keyType: KeyType.Ed25519, + }, + secret: { + privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), + }, + }) + + const didKey = DidKey.fromDid(did.didState.did as string) + const kid = `${did.didState.did as string}#${didKey.key.fingerprint}` + const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) + if (!verificationMethod) throw new Error('No verification method found') + + const w3cCredentialRecords = await agent.modules.openId4VcHolder.requestCredentialUsingPreAuthorizedCode({ + issuerUri: fixture.credentialOffer, + verifyCredentialStatus: false, + // We only allow EdDSa, as we've created a did with keyType ed25519. If we create + // or determine the did dynamically we could use any signature algorithm + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + proofOfPossessionVerificationMethodResolver: () => verificationMethod, + }) + + expect(w3cCredentialRecords).toHaveLength(1) + const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord + expect(w3cCredentialRecord).toBeInstanceOf(W3cCredentialRecord) + + expect(w3cCredentialRecord.credential.type).toEqual([ + 'VerifiableCredential', + 'VerifiableCredentialExtension', + 'OpenBadgeCredential', + ]) + + expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) + }) + + it('[DRAFT 08]: Should successfully execute the pre-authorized flow using a did:key P256 subject and JWT credential', async () => { + const fixture = waltIdJffJwt_draft_08 + + nock('https://jff.walt.id/issuer-api/default/oidc') + // metadata + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.getMetadataResponse) + .get('/.well-known/openid-configuration') + .reply(404) + .get('/.well-known/oauth-authorization-server') + .reply(404) + + // setup access token response + .post('/token') + .reply(200, fixture.credentialResponse) + + // setup credential request response + .post('/credential') + .reply(200, fixture.credentialResponse) + + const did = await agent.dids.create({ + method: 'key', + options: { + keyType: KeyType.P256, + }, + secret: { + privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), + }, + }) + + const didKey = DidKey.fromDid(did.didState.did as string) + const kid = `${didKey.did}#${didKey.key.fingerprint}` + const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) + if (!verificationMethod) throw new Error('No verification method found') + + const w3cCredentialRecords = await agent.modules.openId4VcHolder.requestCredentialUsingPreAuthorizedCode({ + issuerUri: fixture.credentialOffer, + allowedCredentialFormats: [OpenIdCredentialFormatProfile.JwtVcJson], + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], + proofOfPossessionVerificationMethodResolver: () => verificationMethod, + verifyCredentialStatus: false, + }) + + expect(w3cCredentialRecords[0]).toBeInstanceOf(W3cCredentialRecord) + const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord + + expect(w3cCredentialRecord.credential.type).toEqual([ + 'VerifiableCredential', + 'VerifiableAttestation', + 'VerifiableId', + ]) + + expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) + }) + }) + + describe('[DRAFT 08]: Authorization flow', () => { + afterAll(() => { + cleanAll() + enableNetConnect() + }) + + it('[DRAFT 08]: should generate a valid authorization url', async () => { + const fixture = mattrLaunchpadJsonLd_draft_08 + + // setup temporary redirect mock + nock('https://launchpad.mattrlabs.com') + .get('/.well-known/openid-credential-issuer') + .reply(307, undefined, { + Location: 'https://launchpad.vii.electron.mattrlabs.io/.well-known/openid-credential-issuer', + }) + .get('/.well-known/openid-configuration') + .reply(404) + .get('/.well-known/oauth-authorization-server') + .reply(404) + + // setup server metadata response + nock('https://launchpad.vii.electron.mattrlabs.io') + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.getMetadataResponse) + + // setup access token response + .post('/oidc/v1/auth/token') + .reply(200, fixture.acquireAccessTokenResponse) + + // setup credential request response + .post('/oidc/v1/auth/credential') + .reply(200, fixture.credentialResponse) + + const clientId = 'test-client' + + const redirectUri = 'https://example.com/cb' + const scope = ['TestCredential'] + const initiationUri = + 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=OpenBadgeCredential' + const { authorizationUrl } = await agent.modules.openId4VcHolder.generateAuthorizationUrl({ + clientId, + redirectUri, + scope, + initiationUri, + }) + + const parsedUrl = new URL(authorizationUrl) + expect(authorizationUrl.startsWith('https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/authorize')).toBe( + true + ) + expect(parsedUrl.searchParams.get('response_type')).toBe('code') + expect(parsedUrl.searchParams.get('client_id')).toBe(clientId) + expect(parsedUrl.searchParams.get('code_challenge_method')).toBe('S256') + expect(parsedUrl.searchParams.get('redirect_uri')).toBe(redirectUri) + }) + + it('[DRAFT 08]: should throw if no scope is provided', async () => { + const fixture = mattrLaunchpadJsonLd_draft_08 + + // setup temporary redirect mock + nock('https://launchpad.mattrlabs.com').get('/.well-known/openid-credential-issuer').reply(307, undefined, { + Location: 'https://launchpad.vii.electron.mattrlabs.io/.well-known/openid-credential-issuer', + }) + + // setup server metadata response + nock('https://launchpad.vii.electron.mattrlabs.io') + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.getMetadataResponse) + .get('/.well-known/openid-configuration') + .reply(404) + .get('/.well-known/oauth-authorization-server') + .reply(404) + + // setup access token response + .post('/oidc/v1/auth/token') + .reply(200, fixture.acquireAccessTokenResponse) + + // setup credential request response + .post('/oidc/v1/auth/credential') + .reply(200, fixture.credentialResponse) + + // setup server metadata response + nock('https://launchpad.vii.electron.mattrlabs.io') + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.getMetadataResponse) + + const clientId = 'test-client' + const redirectUri = 'https://example.com/cb' + const initiationUri = + 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=OpenBadgeCredential' + await expect( + agent.modules.openId4VcHolder.generateAuthorizationUrl({ + clientId, + redirectUri, + scope: [], + initiationUri, + }) + ).rejects.toThrow() + }) + + // Need custom document loader for this + xit('[DRAFT 08]: should successfully execute request a credential', async () => { + const fixture = mattrLaunchpadJsonLd_draft_08 + + // setup temporary redirect mock + nock('https://launchpad.mattrlabs.com').get('/.well-known/openid-credential-issuer').reply(307, undefined, { + Location: 'https://launchpad.vii.electron.mattrlabs.io/.well-known/openid-credential-issuer', + }) + + // setup server metadata response + nock('https://launchpad.vii.electron.mattrlabs.io') + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.getMetadataResponse) + .get('/.well-known/openid-configuration') + .reply(404) + .get('/.well-known/oauth-authorization-server') + .reply(404) + + // setup access token response + .post('/oidc/v1/auth/token') + .reply(200, fixture.acquireAccessTokenResponse) + + // setup credential request response + .post('/oidc/v1/auth/credential') + .reply(200, fixture.credentialResponse) + + const did = await agent.dids.create({ + method: 'key', + options: { + keyType: KeyType.Ed25519, + }, + secret: { + privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), + }, + }) + + const didKey = DidKey.fromDid(did.didState.did as string) + const kid = `${did.didState.did as string}#${didKey.key.fingerprint}` + const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) + if (!verificationMethod) throw new Error('No verification method found') + + const clientId = 'test-client' + + const redirectUri = 'https://example.com/cb' + const initiationUri = + 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=OpenBadgeCredential' + + const scope = ['TestCredential'] + const { codeVerifier } = await agent.modules.openId4VcHolder.generateAuthorizationUrl({ + clientId, + redirectUri, + scope, + initiationUri, + }) + const w3cCredentialRecords = await agent.modules.openId4VcHolder.requestCredentialUsingAuthorizationCode({ + clientId: clientId, + authorizationCode: 'test-code', + codeVerifier: codeVerifier, + verifyCredentialStatus: false, + proofOfPossessionVerificationMethodResolver: () => verificationMethod, + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + issuerUri: initiationUri, // TODO + redirectUri: redirectUri, + }) + + expect(w3cCredentialRecords).toHaveLength(1) + const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord + expect(w3cCredentialRecord).toBeInstanceOf(W3cCredentialRecord) + + expect(w3cCredentialRecord.credential.type).toEqual([ + 'VerifiableCredential', + 'VerifiableCredentialExtension', + 'OpenBadgeCredential', + ]) + + expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) + }) + }) + + describe('[DRAFT 11]: Pre-authorized flow', () => { + afterEach(() => { + cleanAll() + enableNetConnect() + }) + + // it('[DRAFT 11]: Should successfully execute the pre-authorized flow using a did:key Ed25519 subject and JSON-LD credential', async () => { + // const fixture = waltIdJffJwt_draft_11 + + // nock('https://jff.walt.id/issuer-api/default/oidc') + // .get('/.well-known/openid-credential-issuer') + // .reply(200, fixture.getMetadataResponse) + + // // setup access token response + // .post('/token') + // .reply(200, fixture.credentialResponse) + + // // setup credential request response + // .post('/credential') + // .reply(200, fixture.credentialResponse) + + // const did = await agent.dids.create({ + // method: 'key', + // options: { + // keyType: KeyType.Ed25519, + // }, + // secret: { + // privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), + // }, + // }) + + // const didKey = DidKey.fromDid(did.didState.did as string) + // const kid = `${did.didState.did as string}#${didKey.key.fingerprint}` + // const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) + // if (!verificationMethod) throw new Error('No verification method found') + + // const w3cCredentialRecords = await agent.modules.openId4VcClient.requestCredentialUsingPreAuthorizedCode({ + // uri: fixture.credentialOffer, + // verifyCredentialStatus: false, + // // We only allow EdDSa, as we've created a did with keyType ed25519. If we create + // // or determine the did dynamically we could use any signature algorithm + // allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + // proofOfPossessionVerificationMethodResolver: () => verificationMethod, + // }) + + // expect(w3cCredentialRecords).toHaveLength(1) + // const w3cCredentialRecord = w3cCredentialRecords[0] + // expect(w3cCredentialRecord).toBeInstanceOf(W3cCredentialRecord) + + // expect(w3cCredentialRecord.credential.type).toEqual([ + // 'VerifiableCredential', + // 'VerifiableAttestation', + // 'VerifiableId', + // ]) + + // expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) + // }) + + it('[DRAFT 11]: Should successfully execute the pre-authorized flow using a did:key P256 subject and JWT credential', async () => { + const fixture = waltIdJffJwt_draft_11 + + /** + * Below we're setting up some mock HTTP responses. + * These responses are based on the openid-initiate-issuance URI above + */ + // setup server metadata response + const httpMock = nock('https://jff.walt.id/issuer-api/default/oidc') + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.getMetadataResponse) + .get('/.well-known/openid-configuration') + .reply(404) + .get('/.well-known/oauth-authorization-server') + .reply(404) + + // setup access token response + httpMock.post('/token').reply(200, fixture.credentialResponse) + // setup credential request response + httpMock.post('/credential').reply(200, fixture.credentialResponse) + + const did = await agent.dids.create({ + method: 'key', + options: { + keyType: KeyType.P256, + }, + secret: { + privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), + }, + }) + + const didKey = DidKey.fromDid(did.didState.did as string) + const kid = `${didKey.did}#${didKey.key.fingerprint}` + const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) + if (!verificationMethod) throw new Error('No verification method found') + + const w3cCredentialRecords = await agent.modules.openId4VcHolder.requestCredentialUsingPreAuthorizedCode({ + issuerUri: fixture.credentialOffer, + allowedCredentialFormats: [OpenIdCredentialFormatProfile.JwtVcJson], + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], + proofOfPossessionVerificationMethodResolver: () => verificationMethod, + verifyCredentialStatus: false, + }) + + expect(w3cCredentialRecords[0]).toBeInstanceOf(W3cCredentialRecord) + const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord + + expect(w3cCredentialRecord.credential.type).toEqual([ + 'VerifiableCredential', + 'VerifiableAttestation', + 'VerifiableId', + ]) + + expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) + }) + }) +}) diff --git a/packages/openid4vc-holder/tests/setup.ts b/packages/openid4vc-holder/tests/setup.ts new file mode 100644 index 0000000000..34e38c9705 --- /dev/null +++ b/packages/openid4vc-holder/tests/setup.ts @@ -0,0 +1 @@ +jest.setTimeout(120000) diff --git a/packages/openid4vc-holder/tsconfig.build.json b/packages/openid4vc-holder/tsconfig.build.json new file mode 100644 index 0000000000..2b075bbd85 --- /dev/null +++ b/packages/openid4vc-holder/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "./build", + "skipLibCheck": true + }, + "include": ["src/**/*"] +} diff --git a/packages/openid4vc-holder/tsconfig.json b/packages/openid4vc-holder/tsconfig.json new file mode 100644 index 0000000000..c1aca0e050 --- /dev/null +++ b/packages/openid4vc-holder/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["jest"], + "skipLibCheck": true + } +} From 21bc55979b894b165ab174a23d69dbe4e99d7b8f Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 10 Oct 2023 14:54:40 +0200 Subject: [PATCH 005/115] feat: create issuer, holder and verifier pacakge Signed-off-by: Martin Auer --- packages/openid4vc-client/CHANGELOG.md | 27 - packages/openid4vc-client/README.md | 167 ---- .../src/OpenId4VcClientApi.ts | 53 -- .../src/OpenId4VcClientModule.ts | 34 - .../src/OpenId4VcClientService.ts | 733 ------------------ .../src/OpenId4VcClientServiceOptions.ts | 185 ----- packages/openid4vc-client/src/index.ts | 22 - .../openid4vc-client/src/utils/Formats.ts | 41 - .../src/utils/IssuerMetadataUtils.ts | 113 --- .../__tests__/claimFormatMapping.test.ts | 45 -- .../src/utils/claimFormatMapping.ts | 40 - packages/openid4vc-client/src/utils/index.ts | 2 - .../openid4vc-client/src/utils/metadata.ts | 69 -- .../tests/OpenId4VcClientModule.test.ts | 29 - packages/openid4vc-client/tests/fixtures.ts | 460 ----------- .../tests/openid4vc-client.e2e.test.ts | 483 ------------ packages/openid4vc-holder/package.json | 1 - .../src/OpenId4VcHolderModule.ts | 5 + packages/openid4vc-holder/src/index.ts | 1 + .../presentations/OpenId4VpHolderService.ts} | 2 +- .../PresentationExchangeService.ts | 0 .../src/presentations/example.md | 0 .../src/presentations/fixtures.ts | 0 .../src/presentations/index.ts | 4 +- .../selection/PexCredentialSelection.ts | 0 .../src/presentations/selection/index.ts | 0 .../src/presentations/selection/types.ts | 0 .../src/presentations/transform.ts | 0 .../tests/OpenId4VcClientModule.test.ts | 5 +- packages/openid4vc-issuer/README.md | 68 ++ .../jest.config.ts | 0 .../package.json | 16 +- .../src/OpenId4VcIssuerApi.ts | 27 + .../src/OpenId4VcIssuerModule.ts | 31 + .../src/OpenId4VcIssuerService.ts | 32 + .../src/OpenId4VcIssuerServiceOptions.ts | 7 + packages/openid4vc-issuer/src/index.ts | 8 + .../tests/openid4vc-issuer.e2e.test.ts | 59 ++ .../tests/setup.ts | 0 .../tsconfig.build.json | 0 .../tsconfig.json | 0 packages/openid4vc-verifier/README.md | 68 ++ packages/openid4vc-verifier/jest.config.ts | 14 + packages/openid4vc-verifier/package.json | 41 + .../src/OpenId4VcVerifierApi.ts | 27 + .../src/OpenId4VcVerifierModule.ts | 31 + .../src/OpenId4VcVerifierService.ts | 32 + .../src/OpenId4VcVerifierServiceOptions.ts | 7 + packages/openid4vc-verifier/src/index.ts | 8 + .../tests/openid4vc-verifier.e2e.test.ts | 59 ++ packages/openid4vc-verifier/tests/setup.ts | 1 + .../openid4vc-verifier/tsconfig.build.json | 8 + packages/openid4vc-verifier/tsconfig.json | 7 + 53 files changed, 553 insertions(+), 2519 deletions(-) delete mode 100644 packages/openid4vc-client/CHANGELOG.md delete mode 100644 packages/openid4vc-client/README.md delete mode 100644 packages/openid4vc-client/src/OpenId4VcClientApi.ts delete mode 100644 packages/openid4vc-client/src/OpenId4VcClientModule.ts delete mode 100644 packages/openid4vc-client/src/OpenId4VcClientService.ts delete mode 100644 packages/openid4vc-client/src/OpenId4VcClientServiceOptions.ts delete mode 100644 packages/openid4vc-client/src/index.ts delete mode 100644 packages/openid4vc-client/src/utils/Formats.ts delete mode 100644 packages/openid4vc-client/src/utils/IssuerMetadataUtils.ts delete mode 100644 packages/openid4vc-client/src/utils/__tests__/claimFormatMapping.test.ts delete mode 100644 packages/openid4vc-client/src/utils/claimFormatMapping.ts delete mode 100644 packages/openid4vc-client/src/utils/index.ts delete mode 100644 packages/openid4vc-client/src/utils/metadata.ts delete mode 100644 packages/openid4vc-client/tests/OpenId4VcClientModule.test.ts delete mode 100644 packages/openid4vc-client/tests/fixtures.ts delete mode 100644 packages/openid4vc-client/tests/openid4vc-client.e2e.test.ts rename packages/{openid4vc-client/src/presentations/OpenId4VpClientService.ts => openid4vc-holder/src/presentations/OpenId4VpHolderService.ts} (99%) rename packages/{openid4vc-client => openid4vc-holder}/src/presentations/PresentationExchangeService.ts (100%) rename packages/{openid4vc-client => openid4vc-holder}/src/presentations/example.md (100%) rename packages/{openid4vc-client => openid4vc-holder}/src/presentations/fixtures.ts (100%) rename packages/{openid4vc-client => openid4vc-holder}/src/presentations/index.ts (78%) rename packages/{openid4vc-client => openid4vc-holder}/src/presentations/selection/PexCredentialSelection.ts (100%) rename packages/{openid4vc-client => openid4vc-holder}/src/presentations/selection/index.ts (100%) rename packages/{openid4vc-client => openid4vc-holder}/src/presentations/selection/types.ts (100%) rename packages/{openid4vc-client => openid4vc-holder}/src/presentations/transform.ts (100%) create mode 100644 packages/openid4vc-issuer/README.md rename packages/{openid4vc-client => openid4vc-issuer}/jest.config.ts (100%) rename packages/{openid4vc-client => openid4vc-issuer}/package.json (69%) create mode 100644 packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts create mode 100644 packages/openid4vc-issuer/src/OpenId4VcIssuerModule.ts create mode 100644 packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts create mode 100644 packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts create mode 100644 packages/openid4vc-issuer/src/index.ts create mode 100644 packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts rename packages/{openid4vc-client => openid4vc-issuer}/tests/setup.ts (100%) rename packages/{openid4vc-client => openid4vc-issuer}/tsconfig.build.json (100%) rename packages/{openid4vc-client => openid4vc-issuer}/tsconfig.json (100%) create mode 100644 packages/openid4vc-verifier/README.md create mode 100644 packages/openid4vc-verifier/jest.config.ts create mode 100644 packages/openid4vc-verifier/package.json create mode 100644 packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts create mode 100644 packages/openid4vc-verifier/src/OpenId4VcVerifierModule.ts create mode 100644 packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts create mode 100644 packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts create mode 100644 packages/openid4vc-verifier/src/index.ts create mode 100644 packages/openid4vc-verifier/tests/openid4vc-verifier.e2e.test.ts create mode 100644 packages/openid4vc-verifier/tests/setup.ts create mode 100644 packages/openid4vc-verifier/tsconfig.build.json create mode 100644 packages/openid4vc-verifier/tsconfig.json diff --git a/packages/openid4vc-client/CHANGELOG.md b/packages/openid4vc-client/CHANGELOG.md deleted file mode 100644 index 0c03004490..0000000000 --- a/packages/openid4vc-client/CHANGELOG.md +++ /dev/null @@ -1,27 +0,0 @@ -# Change Log - -All notable changes to this project will be documented in this file. -See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. - -## [0.4.2](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.1...v0.4.2) (2023-10-05) - -**Note:** Version bump only for package @aries-framework/openid4vc-client - -## [0.4.1](https://github.com/hyperledger/aries-framework-javascript/compare/v0.4.0...v0.4.1) (2023-08-28) - -**Note:** Version bump only for package @aries-framework/openid4vc-client - -# [0.4.0](https://github.com/hyperledger/aries-framework-javascript/compare/v0.3.3...v0.4.0) (2023-06-03) - -### Bug Fixes - -- remove scope check from response ([#1450](https://github.com/hyperledger/aries-framework-javascript/issues/1450)) ([7dd4061](https://github.com/hyperledger/aries-framework-javascript/commit/7dd406170c75801529daf4bebebde81e84a4cb79)) - -### Features - -- **core:** add W3cCredentialsApi ([c888736](https://github.com/hyperledger/aries-framework-javascript/commit/c888736cb6b51014e23f5520fbc4074cf0e49e15)) -- **openid4vc-client:** openid authorization flow ([#1384](https://github.com/hyperledger/aries-framework-javascript/issues/1384)) ([996c08f](https://github.com/hyperledger/aries-framework-javascript/commit/996c08f8e32e58605408f5ed5b6d8116cea3b00c)) -- **openid4vc-client:** pre-authorized ([#1243](https://github.com/hyperledger/aries-framework-javascript/issues/1243)) ([3d86e78](https://github.com/hyperledger/aries-framework-javascript/commit/3d86e78a4df87869aa5df4e28b79cd91787b61fb)) -- **openid4vc:** jwt format and more crypto ([#1472](https://github.com/hyperledger/aries-framework-javascript/issues/1472)) ([bd4932d](https://github.com/hyperledger/aries-framework-javascript/commit/bd4932d34f7314a6d49097b6460c7570e1ebc7a8)) -- outbound message send via session ([#1335](https://github.com/hyperledger/aries-framework-javascript/issues/1335)) ([582c711](https://github.com/hyperledger/aries-framework-javascript/commit/582c711728db12b7d38a0be2e9fa78dbf31b34c6)) -- support more key types in jws service ([#1453](https://github.com/hyperledger/aries-framework-javascript/issues/1453)) ([8a3f03e](https://github.com/hyperledger/aries-framework-javascript/commit/8a3f03eb0dffcf46635556defdcebe1d329cf428)) diff --git a/packages/openid4vc-client/README.md b/packages/openid4vc-client/README.md deleted file mode 100644 index 540339fef7..0000000000 --- a/packages/openid4vc-client/README.md +++ /dev/null @@ -1,167 +0,0 @@ -

-
- Hyperledger Aries logo -

-

Aries Framework JavaScript Open ID Connect For Verifiable Credentials Client Module

-

- License - typescript - @aries-framework/openid4vc-client version - -

-
- -Open ID Connect For Verifiable Credentials Client Module for [Aries Framework JavaScript](https://github.com/hyperledger/aries-framework-javascript). - -### Installation - -Make sure you have set up the correct version of Aries Framework JavaScript according to the AFJ repository. - -```sh -yarn add @aries-framework/openid4vc-client -``` - -### Quick start - -#### Requirements - -Before a credential can be requested, you need the issuer URI. This URI starts with `openid-initiate-issuance://` and is provided by the issuer. The issuer URI is commonly acquired by scanning a QR code. - -#### Module registration - -In order to get this module to work, we need to inject it into the agent. This makes the module's functionality accessible through the agent's `modules` api. - -```ts -import { OpenId4VcClientModule } from '@aries-framework/openid4vc-client' - -const agent = new Agent({ - config: { - /* config */ - }, - dependencies: agentDependencies, - modules: { - openId4VcClient: new OpenId4VcClientModule(), - /* other custom modules */ - }, -}) - -await agent.initialize() -``` - -How the module is injected and the agent has been initialized, you can access the module's functionality through `agent.modules.openId4VcClient`. - -#### Preparing a DID - -In order to request a credential, you'll need to provide a DID that the issuer will use for setting the credential subject. In the following snippet we create one for the sake of the example, but this can be any DID that has a _authentication verification method_ with key type `Ed25519`. - -```ts -// first we create the DID -const did = await agent.dids.create({ - method: 'key', - options: { - keyType: KeyType.Ed25519, - }, -}) - -// next we do some assertions and extract the key identifier (kid) - -if ( - !did.didState.didDocument || - !did.didState.didDocument.authentication || - did.didState.didDocument.authentication.length === 0 -) { - throw new Error("Error creating did document, or did document has no 'authentication' verificationMethods") -} - -const [verificationMethod] = did.didState.didDocument.authentication -const kid = typeof verificationMethod === 'string' ? verificationMethod : verificationMethod.id -``` - -#### Requesting the credential (Pre-Authorized) - -Now a credential issuance can be requested as follows. - -```ts -const w3cCredentialRecord = await agent.modules.openId4VcClient.requestCredentialPreAuthorized({ - issuerUri, - kid, - checkRevocationState: false, -}) - -console.log(w3cCredentialRecord) -``` - -#### Full example - -```ts -import { OpenId4VcClientModule } from '@aries-framework/openid4vc-client' -import { agentDependencies } from '@aries-framework/node' // use @aries-framework/react-native for React Native -import { Agent, KeyDidCreateOptions } from '@aries-framework/core' - -const run = async () => { - const issuerUri = '' // The obtained issuer URI - - // Create the Agent - const agent = new Agent({ - config: { - /* config */ - }, - dependencies: agentDependencies, - modules: { - openId4VcClient: new OpenId4VcClientModule(), - /* other custom modules */ - }, - }) - - // Initialize the Agent - await agent.initialize() - - // Create a DID - const did = await agent.dids.create({ - method: 'key', - options: { - keyType: KeyType.Ed25519, - }, - }) - - // Assert DIDDocument is valid - if ( - !did.didState.didDocument || - !did.didState.didDocument.authentication || - did.didState.didDocument.authentication.length === 0 - ) { - throw new Error("Error creating did document, or did document has no 'authentication' verificationMethods") - } - - // Extract key identified (kid) for authentication verification method - const [verificationMethod] = did.didState.didDocument.authentication - const kid = typeof verificationMethod === 'string' ? verificationMethod : verificationMethod.id - - // Request the credential - const w3cCredentialRecord = await agent.modules.openId4VcClient.requestCredentialPreAuthorized({ - issuerUri, - kid, - checkRevocationState: false, - }) - - // Log the received credential - console.log(w3cCredentialRecord) -} -``` diff --git a/packages/openid4vc-client/src/OpenId4VcClientApi.ts b/packages/openid4vc-client/src/OpenId4VcClientApi.ts deleted file mode 100644 index 5049c518ba..0000000000 --- a/packages/openid4vc-client/src/OpenId4VcClientApi.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { - GenerateAuthorizationUrlOptions, - PreAuthCodeFlowOptions, - AuthCodeFlowOptions, -} from './OpenId4VcClientServiceOptions' -import type { W3cCredentialRecord } from '@aries-framework/core' - -import { injectable, AgentContext } from '@aries-framework/core' - -import { OpenId4VcClientService } from './OpenId4VcClientService' -import { AuthFlowType } from './OpenId4VcClientServiceOptions' - -/** - * @public - */ -@injectable() -export class OpenId4VcClientApi { - private agentContext: AgentContext - private openId4VcClientService: OpenId4VcClientService - - public constructor(agentContext: AgentContext, openId4VcClientService: OpenId4VcClientService) { - this.agentContext = agentContext - this.openId4VcClientService = openId4VcClientService - } - - public async requestCredentialUsingPreAuthorizedCode( - options: PreAuthCodeFlowOptions - ): Promise { - // set defaults - const verifyRevocationState = options.verifyCredentialStatus ?? true - - return this.openId4VcClientService.requestCredential(this.agentContext, { - ...options, - verifyCredentialStatus: verifyRevocationState, - flowType: AuthFlowType.PreAuthorizedCodeFlow, - }) - } - - public async requestCredentialUsingAuthorizationCode(options: AuthCodeFlowOptions): Promise { - // set defaults - const checkRevocationState = options.verifyCredentialStatus ?? true - - return this.openId4VcClientService.requestCredential(this.agentContext, { - ...options, - verifyCredentialStatus: checkRevocationState, - flowType: AuthFlowType.AuthorizationCodeFlow, - }) - } - - public async generateAuthorizationUrl(options: GenerateAuthorizationUrlOptions) { - return this.openId4VcClientService.generateAuthorizationUrl(options) - } -} diff --git a/packages/openid4vc-client/src/OpenId4VcClientModule.ts b/packages/openid4vc-client/src/OpenId4VcClientModule.ts deleted file mode 100644 index 6eb598b3d3..0000000000 --- a/packages/openid4vc-client/src/OpenId4VcClientModule.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { DependencyManager, Module } from '@aries-framework/core' - -import { AgentConfig } from '@aries-framework/core' - -import { OpenId4VcClientApi } from './OpenId4VcClientApi' -import { OpenId4VcClientService } from './OpenId4VcClientService' -import { OpenId4VpClientService, PresentationExchangeService } from './presentations' - -/** - * @public - */ -export class OpenId4VcClientModule implements Module { - public readonly api = OpenId4VcClientApi - - /** - * Registers the dependencies of the question answer module on the dependency manager. - */ - public register(dependencyManager: DependencyManager) { - // Warn about experimental module - dependencyManager - .resolve(AgentConfig) - .logger.warn( - "The '@aries-framework/openid4vc-client' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." - ) - - // Api - dependencyManager.registerContextScoped(OpenId4VcClientApi) - - // Services - dependencyManager.registerSingleton(OpenId4VcClientService) - dependencyManager.registerSingleton(OpenId4VpClientService) - dependencyManager.registerSingleton(PresentationExchangeService) - } -} diff --git a/packages/openid4vc-client/src/OpenId4VcClientService.ts b/packages/openid4vc-client/src/OpenId4VcClientService.ts deleted file mode 100644 index e8d7247e2f..0000000000 --- a/packages/openid4vc-client/src/OpenId4VcClientService.ts +++ /dev/null @@ -1,733 +0,0 @@ -import type { - GenerateAuthorizationUrlOptions, - RequestCredentialOptions, - ProofOfPossessionVerificationMethodResolver, - SupportedCredentialFormats, - ProofOfPossessionRequirements, -} from './OpenId4VcClientServiceOptions' -import type { OpenIdCredentialFormatProfile } from './utils' -import type { - AgentContext, - W3cVerifiableCredential, - VerificationMethod, - JwaSignatureAlgorithm, - W3cVerifyCredentialResult, -} from '@aries-framework/core' -import type { - CredentialOfferFormat, - CredentialOfferPayloadV1_0_08, - CredentialOfferRequestWithBaseUrl, - CredentialResponse, - CredentialSupported, - Jwt, - OpenIDResponse, - ProofOfPossessionCallbacks, -} from '@sphereon/oid4vci-common' - -import { - W3cCredentialRecord, - ClaimFormat, - getJwkClassFromJwaSignatureAlgorithm, - W3cJwtVerifiableCredential, - AriesFrameworkError, - getKeyFromVerificationMethod, - Hasher, - inject, - injectable, - InjectionSymbols, - JsonEncoder, - JsonTransformer, - TypedArrayEncoder, - W3cJsonLdVerifiableCredential, - getJwkFromKey, - getSupportedVerificationMethodTypesFromKeyType, - getJwkClassFromKeyType, - parseDid, - SignatureSuiteRegistry, - JwsService, - Logger, - W3cCredentialService, - W3cCredentialRepository, -} from '@aries-framework/core' -import { CredentialRequestClientBuilder, OpenID4VCIClient, ProofOfPossessionBuilder } from '@sphereon/oid4vci-client' -import { AuthzFlowType, CodeChallengeMethod, OpenId4VCIVersion } from '@sphereon/oid4vci-common' -import { randomStringForEntropy } from '@stablelib/random' - -import { supportedCredentialFormats, AuthFlowType } from './OpenId4VcClientServiceOptions' -import { setOpenId4VcCredentialMetadata, fromOpenIdCredentialFormatProfileToDifClaimFormat } from './utils' -import { getUniformFormat } from './utils/Formats' -import { getSupportedCredentials } from './utils/IssuerMetadataUtils' - -/** - * The type of a credential offer entry. For each item in `credentials` array, the type MUST be one of the following: - * - CredentialSupported, when the value is a string and points to a credential from the `credentials_supported` array. - * - InlineCredentialOffer, when the value is a JSON object that represents an inline credential offer. - */ -export enum OfferedCredentialType { - CredentialSupported = 'CredentialSupported', - InlineCredentialOffer = 'InlineCredentialOffer', -} - -export type OfferedCredentialsWithMetadata = - | { credentialSupported: CredentialSupported; type: OfferedCredentialType.CredentialSupported } - | { inlineCredentialOffer: CredentialOfferFormat; type: OfferedCredentialType.InlineCredentialOffer } - -const flowTypeMapping = { - [AuthFlowType.AuthorizationCodeFlow]: AuthzFlowType.AUTHORIZATION_CODE_FLOW, - [AuthFlowType.PreAuthorizedCodeFlow]: AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW, -} - -/** - * @internal - */ -@injectable() -export class OpenId4VcClientService { - private logger: Logger - private w3cCredentialService: W3cCredentialService - private w3cCredentialRepository: W3cCredentialRepository - private jwsService: JwsService - - public constructor( - @inject(InjectionSymbols.Logger) logger: Logger, - w3cCredentialService: W3cCredentialService, - w3cCredentialRepository: W3cCredentialRepository, - jwsService: JwsService - ) { - this.w3cCredentialService = w3cCredentialService - this.w3cCredentialRepository = w3cCredentialRepository - this.jwsService = jwsService - this.logger = logger - } - - private generateCodeVerifier(): string { - return randomStringForEntropy(256) - } - - public async generateAuthorizationUrl(options: GenerateAuthorizationUrlOptions) { - this.logger.debug('Generating authorization url') - - if (!options.scope || options.scope.length === 0) { - throw new AriesFrameworkError( - 'Only scoped based authorization requests are supported at this time. Please provide at least one scope' - ) - } - - const client = await OpenID4VCIClient.fromURI({ - uri: options.initiationUri, - flowType: AuthzFlowType.AUTHORIZATION_CODE_FLOW, - }) - - const codeVerifier = this.generateCodeVerifier() - const codeVerifierSha256 = Hasher.hash(TypedArrayEncoder.fromString(codeVerifier), 'sha2-256') - const base64Url = TypedArrayEncoder.toBase64URL(codeVerifierSha256) - - this.logger.debug('Converted code_verifier to code_challenge', { - codeVerifier: codeVerifier, - sha256: codeVerifierSha256.toString(), - base64Url: base64Url, - }) - - const authorizationUrl = client.createAuthorizationRequestUrl({ - clientId: options.clientId, - codeChallengeMethod: CodeChallengeMethod.SHA256, - codeChallenge: base64Url, - redirectUri: options.redirectUri, - scope: options.scope?.join(' '), - }) - - return { - authorizationUrl, - codeVerifier, - } - } - - public async requestCredential(agentContext: AgentContext, options: RequestCredentialOptions) { - const receivedCredentials: W3cCredentialRecord[] = [] - const supportedJwaSignatureAlgorithms = this.getSupportedJwaSignatureAlgorithms(agentContext) - - const allowedProofOfPossessionSignatureAlgorithms = options.allowedProofOfPossessionSignatureAlgorithms - ? options.allowedProofOfPossessionSignatureAlgorithms.filter((algorithm) => - supportedJwaSignatureAlgorithms.includes(algorithm) - ) - : supportedJwaSignatureAlgorithms - - // Take the allowed credential formats from the options or use the default - const allowedCredentialFormats = options.allowedCredentialFormats ?? supportedCredentialFormats - - const flowType = flowTypeMapping[options.flowType] - if (!flowType) { - throw new AriesFrameworkError( - `Unsupported flowType ${options.flowType}. Valid values are ${Object.values(AuthFlowType).join(', ')}` - ) - } - - const client = await OpenID4VCIClient.fromURI({ - uri: options.issuerUri, - flowType, - retrieveServerMetadata: false, - }) - - const serverMetadata = await client.retrieveServerMetadata() - - this.logger.info('Fetched server metadata', { - issuer: serverMetadata.issuer, - credentialEndpoint: serverMetadata.credential_endpoint, - tokenEndpoint: serverMetadata.token_endpoint, - }) - - this.logger.debug('Full server metadata', serverMetadata) - - // acquire the access token - // NOTE: only scope based flow is supported for authorized flow. However there's not clear mapping between - // the scope property and which credential to request (this is out of scope of the spec), so it will still - // just request all credentials that have been offered in the credential offer. We may need to add some extra - // input properties that allows to define the credential type(s) to request. - const accessToken = - options.flowType === AuthFlowType.AuthorizationCodeFlow - ? await client.acquireAccessToken({ - clientId: options.clientId, - code: options.authorizationCode, - codeVerifier: options.codeVerifier, - redirectUri: options.redirectUri, - }) - : await client.acquireAccessToken({}) // TODO: PIN - - // Loop through all the credentialTypes in the credential offer - for (const offeredCredential of this.getOfferedCredentialsWithMetadata(client)) { - const format = ( - isInlineCredentialOffer(offeredCredential) - ? offeredCredential.inlineCredentialOffer.format - : offeredCredential.credentialSupported.format - ) as SupportedCredentialFormats // TODO: can we remove the cast? - - // TODO: support inline credential offers. Not clear to me how to determine the did method / alg, etc.. - if (offeredCredential.type === OfferedCredentialType.InlineCredentialOffer) { - // Check if the format is supported/allowed - if (!allowedCredentialFormats.includes(format)) continue - } else { - const supportedCredentialMetadata = offeredCredential.credentialSupported - - // FIXME - // TODO: that is not a must v11 could end in the same way - // If the credential id ends with the format, it is a v8 credential supported that has been - // split into multiple entries (each entry can now only have one format). For now we continue - // as assume there will be another entry with the correct format. - if (supportedCredentialMetadata.id?.endsWith(`-${supportedCredentialMetadata.format}`)) { - const uniformFormat = getUniformFormat(supportedCredentialMetadata.format) as SupportedCredentialFormats - if (!allowedCredentialFormats.includes(uniformFormat)) continue - } - } - - // Get all options for the credential request (such as which kid to use, the signature algorithm, etc) - const { verificationMethod, signatureAlgorithm } = await this.getCredentialRequestOptions(agentContext, { - allowedCredentialFormats, - allowedProofOfPossessionSignatureAlgorithms, - offeredCredentialWithMetadata: offeredCredential, - proofOfPossessionVerificationMethodResolver: options.proofOfPossessionVerificationMethodResolver, - }) - - const callbacks: ProofOfPossessionCallbacks = { - signCallback: this.signCallback(agentContext, verificationMethod), - // TODO: verify callback - } - - // Create the proof of possession - const proofInput = await ProofOfPossessionBuilder.fromAccessTokenResponse({ - accessTokenResponse: accessToken, - callbacks, - version: client.version(), - }) - .withEndpointMetadata(serverMetadata) - .withAlg(signatureAlgorithm) - .withClientId(verificationMethod.controller) - .withKid(verificationMethod.id) - .build() - - this.logger.debug('Generated JWS', proofInput) - - // Acquire the credential - const credentialRequestClient = // TODO: don't use the uri not actual anymore https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-08.html - ( - await CredentialRequestClientBuilder.fromURI({ - uri: options.issuerUri, - metadata: serverMetadata, - }) - ) - .withTokenFromResponse(accessToken) - .build() - - let credentialResponse: OpenIDResponse - - if (isInlineCredentialOffer(offeredCredential)) { - credentialResponse = await credentialRequestClient.acquireCredentialsUsingProof({ - proofInput, - credentialTypes: offeredCredential.inlineCredentialOffer.types, - format: offeredCredential.inlineCredentialOffer.format, - }) - } else { - credentialResponse = await credentialRequestClient.acquireCredentialsUsingProof({ - proofInput, - credentialTypes: offeredCredential.type, - format: offeredCredential.credentialSupported.format, - }) - } - - const credential = await this.handleCredentialResponse(agentContext, credentialResponse, { - verifyCredentialStatus: options.verifyCredentialStatus, - }) - - // Create credential record, but we don't store it yet (only after the user has accepted the credential) - const credentialRecord = new W3cCredentialRecord({ - credential, - tags: { - expandedTypes: [], - }, - }) - this.logger.debug('Full credential', credentialRecord) - - if (!isInlineCredentialOffer(offeredCredential)) { - const issuerMetadata = client.endpointMetadata.credentialIssuerMetadata - if (!issuerMetadata) { - // TODO: this should not happen - throw new AriesFrameworkError('Issuer metadata not found') - } - const supportedCredentialMetadata = offeredCredential.credentialSupported - // Set the OpenId4Vc credential metadata and update record - setOpenId4VcCredentialMetadata(credentialRecord, supportedCredentialMetadata, serverMetadata, issuerMetadata) - } - - receivedCredentials.push(credentialRecord) - } - - return receivedCredentials - } - - /** - * Get the options for the credential request. Internally this will resolve the proof of possession - * requirements, and based on that it will call the proofOfPossessionVerificationMethodResolver to - * allow the caller to select the correct verification method based on the requirements for the proof - * of possession. - */ - private async getCredentialRequestOptions( - agentContext: AgentContext, - options: { - proofOfPossessionVerificationMethodResolver: ProofOfPossessionVerificationMethodResolver - allowedCredentialFormats: SupportedCredentialFormats[] - allowedProofOfPossessionSignatureAlgorithms: JwaSignatureAlgorithm[] - offeredCredentialWithMetadata: OfferedCredentialsWithMetadata - } - ) { - const { signatureAlgorithm, supportedDidMethods, supportsAllDidMethods } = this.getProofOfPossessionRequirements( - agentContext, - { - offeredCredentialWithMetadata: options.offeredCredentialWithMetadata, - allowedCredentialFormats: options.allowedCredentialFormats, - allowedProofOfPossessionSignatureAlgorithms: options.allowedProofOfPossessionSignatureAlgorithms, - } - ) - - const JwkClass = getJwkClassFromJwaSignatureAlgorithm(signatureAlgorithm) - - if (!JwkClass) { - throw new AriesFrameworkError( - `Could not determine JWK key type based on JWA signature algorithm '${signatureAlgorithm}'` - ) - } - - const supportedVerificationMethods = getSupportedVerificationMethodTypesFromKeyType(JwkClass.keyType) - - const format = isInlineCredentialOffer(options.offeredCredentialWithMetadata) - ? options.offeredCredentialWithMetadata.inlineCredentialOffer.format - : options.offeredCredentialWithMetadata.credentialSupported.format - - // Now we need to determine the did method and alg based on the cryptographic suite - const verificationMethod = await options.proofOfPossessionVerificationMethodResolver({ - credentialFormat: format as SupportedCredentialFormats, - proofOfPossessionSignatureAlgorithm: signatureAlgorithm, - supportedVerificationMethods, - keyType: JwkClass.keyType, - supportedCredentialId: !isInlineCredentialOffer(options.offeredCredentialWithMetadata) - ? options.offeredCredentialWithMetadata.credentialSupported.id - : undefined, - supportsAllDidMethods, - supportedDidMethods, - }) - - // Make sure the verification method uses a supported did method - if ( - !supportsAllDidMethods && - // If supportedDidMethods is undefined, it means the issuer didn't include the binding methods in the metadata - // The user can still select a verification method, but we can't validate it - supportedDidMethods !== undefined && - !supportedDidMethods.find((supportedDidMethod) => verificationMethod.id.startsWith(supportedDidMethod)) - ) { - const { method } = parseDid(verificationMethod.id) - const supportedDidMethodsString = supportedDidMethods.join(', ') - throw new AriesFrameworkError( - `Verification method uses did method '${method}', but issuer only supports '${supportedDidMethodsString}'` - ) - } - - // Make sure the verification method uses a supported verification method type - if (!supportedVerificationMethods.includes(verificationMethod.type)) { - const supportedVerificationMethodsString = supportedVerificationMethods.join(', ') - throw new AriesFrameworkError( - `Verification method uses verification method type '${verificationMethod.type}', but only '${supportedVerificationMethodsString}' verification methods are supported for key type '${JwkClass.keyType}'` - ) - } - - return { verificationMethod, signatureAlgorithm } - } - - // TODO: i cannot view this - // todo https://sphereon.atlassian.net/browse/VDX-184 - /** - * Returns all entries from the credential offer. This includes both 'id' entries that reference a supported credential in the issuer metadata, - * as well as inline credential offers that do not reference a supported credential in the issuer metadata. - */ - private getOfferedCredentials( - credentialOfferRequestWithBaseUrl: CredentialOfferRequestWithBaseUrl - ): Array { - if (credentialOfferRequestWithBaseUrl.version < OpenId4VCIVersion.VER_1_0_11) { - const credentialOffer = - credentialOfferRequestWithBaseUrl.original_credential_offer as CredentialOfferPayloadV1_0_08 - - return typeof credentialOffer.credential_type === 'string' - ? [credentialOffer.credential_type] - : credentialOffer.credential_type - } else { - return credentialOfferRequestWithBaseUrl.credential_offer.credentials - } - } - - /** - * Return a normalized version of the credentials supported by the issuer. Can optionally filter based on the credentials - * that were offered, or the type of credentials that are supported. - * - * - * NOTE: for v1_0-08, a single credential id in the issuer metadata could have multiple formats. When retrieving the - * supported credentials, for v1_0-08, the format is appended to the id if there are multiple formats supported for - * that credential id. E.g. if the issuer metadata for v1_0-08 contains an entry with key `OpenBadgeCredential` and - * the supported formats are `jwt_vc-jsonld` and `ldp_vc`, then the id in the credentials supported will be - * `OpenBadgeCredential-jwt_vc-jsonld` and `OpenBadgeCredential-ldp_vc`, even though the offered credential is simply - * `OpenBadgeCredential`. - * - * NOTE: this method only returns the credentials supported by the issuer metadata. It does not take into account the inline - * credentials offered. Use {@link getOfferedCredentialsWithMetadata} to get both the inline and referenced offered credentials. - */ - private getCredentialsSupported( - client: OpenID4VCIClient, - restrictToOfferIds: boolean, - credentialSupportedId?: string - ): CredentialSupported[] { - const offeredIds = this.getOfferedCredentials(client.credentialOffer).filter( - (c): c is string => typeof c === 'string' - ) - - const credentialSupportedIds = restrictToOfferIds ? offeredIds : undefined - - const credentialsSupported = getSupportedCredentials({ - issuerMetadata: client.endpointMetadata.credentialIssuerMetadata, - version: client.version(), - credentialSupportedIds, - }) - - return credentialSupportedId - ? credentialsSupported.filter( - (credentialSupported) => - credentialSupported.id === credentialSupportedId || - credentialSupported.id === `${credentialSupportedId}-${credentialSupported.format}` - ) - : credentialsSupported - } - - /** - * Returns all entries from the credential offer with the associated metadata resolved. For inline entries, the offered credential object - * is included directly. For 'id' entries, the associated `credentials_supported` object is resolved from the issuer metadata. - * - * NOTE: for v1_0-08, a single credential id in the issuer metadata could have multiple formats. This means that the returned value - * from this method could contain multiple entries for a single credential id, but with different formats. This is detectable as the - * id will be the `-`. - */ - private getOfferedCredentialsWithMetadata = (client: OpenID4VCIClient) => { - const offeredCredentials: Array = [] - - for (const offeredCredential of this.getOfferedCredentials(client.credentialOffer)) { - // If the offeredCredential is a string, it references a supported credential in the issuer metadata - if (typeof offeredCredential === 'string') { - const credentialsSupported = this.getCredentialsSupported(client, false, offeredCredential) - - // Make sure the issuer metadata includes the offered credential. - if (credentialsSupported.length === 0) { - throw new Error( - `Offered credential '${offeredCredential}' is not present in the credentials_supported of the issuer metadata` - ) - } - - offeredCredentials.push( - ...credentialsSupported.map((credentialSupported) => { - return { credentialSupported, type: OfferedCredentialType.CredentialSupported } as const - }) - ) - } - // Otherwise it's an inline credential offer that does not reference a supported credential in the issuer metadata - else { - // TODO: we could transform the inline offer to the `CredentialSupported` format, but we'll only be able to populate - // the `format`, `types` and `@context` fields. It's not really clear how to determine the supported did methods, - // signature suites, etc.. for these inline credentials. - // We should also add a property to indicate to the user that this is an inline credential offer. - // if (offeredCredential.format === 'jwt_vc_json') { - // const supported = { - // format: offeredCredential.format, - // types: offeredCredential.types, - // } satisfies CredentialSupportedJwtVcJson; - // } else if (offeredCredential.format === 'jwt_vc_json-ld' || offeredCredential.format === 'ldp_vc') { - // const supported = { - // format: offeredCredential.format, - // '@context': offeredCredential.credential_definition['@context'], - // types: offeredCredential.credential_definition.types, - // } satisfies CredentialSupported; - // } - offeredCredentials.push({ - inlineCredentialOffer: offeredCredential, - type: OfferedCredentialType.InlineCredentialOffer, - } as const) - } - } - - return offeredCredentials - } - - /** - * Get the requirements for creating the proof of possession. Based on the allowed - * credential formats, the allowed proof of possession signature algorithms, and the - * credential type, this method will select the best credential format and signature - * algorithm to use, based on the order of preference. - */ - private getProofOfPossessionRequirements( - agentContext: AgentContext, - options: { - allowedCredentialFormats: SupportedCredentialFormats[] - offeredCredentialWithMetadata: OfferedCredentialsWithMetadata - allowedProofOfPossessionSignatureAlgorithms: JwaSignatureAlgorithm[] - } - ): ProofOfPossessionRequirements { - const { offeredCredentialWithMetadata, allowedCredentialFormats } = options - - // Extract format from offer - let format = - offeredCredentialWithMetadata.type === OfferedCredentialType.InlineCredentialOffer - ? offeredCredentialWithMetadata.inlineCredentialOffer.format - : offeredCredentialWithMetadata.credentialSupported.format - - // Get uniform format, so we don't have to deal with the different spec versions - format = getUniformFormat(format) - - const credentialMetadata = - offeredCredentialWithMetadata.type === OfferedCredentialType.CredentialSupported - ? offeredCredentialWithMetadata.credentialSupported - : undefined - - const issuerSupportedCryptographicSuites = credentialMetadata?.cryptographic_suites_supported - const issuerSupportedBindingMethods = - credentialMetadata?.cryptographic_binding_methods_supported ?? - // FIXME: somehow the MATTR Launchpad returns binding_methods_supported instead of cryptographic_binding_methods_supported - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - (credentialMetadata?.binding_methods_supported as string[] | undefined) - - if (!isInlineCredentialOffer(offeredCredentialWithMetadata)) { - const credentialMetadata = offeredCredentialWithMetadata.credentialSupported - if (!allowedCredentialFormats.includes(format as SupportedCredentialFormats)) { - throw new AriesFrameworkError( - `Issuer only supports format '${format}' for credential type '${ - credentialMetadata.id as string - }', but the wallet only allows formats '${options.allowedCredentialFormats.join(', ')}'` - ) - } - } - - // For each of the supported algs, find the key types, then find the proof types - const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) - - let potentialSignatureAlgorithm: JwaSignatureAlgorithm | undefined - - switch (format) { - case 'jwt_vc_json': - case 'jwt_vc_json-ld': - // If undefined, it means the issuer didn't include the cryptographic suites in the metadata - // We just guess that the first one is supported - if (issuerSupportedCryptographicSuites === undefined) { - potentialSignatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms[0] - } else { - potentialSignatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms.find((signatureAlgorithm) => - issuerSupportedCryptographicSuites.includes(signatureAlgorithm) - ) - } - break - case 'ldp_vc': - // If undefined, it means the issuer didn't include the cryptographic suites in the metadata - // We just guess that the first one is supported - if (issuerSupportedCryptographicSuites === undefined) { - potentialSignatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms[0] - } else { - // We need to find it based on the JSON-LD proof type - potentialSignatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms.find( - (signatureAlgorithm) => { - const JwkClass = getJwkClassFromJwaSignatureAlgorithm(signatureAlgorithm) - if (!JwkClass) return false - - // TODO: getByKeyType should return a list - const matchingSuite = signatureSuiteRegistry.getByKeyType(JwkClass.keyType) - if (!matchingSuite) return false - - return issuerSupportedCryptographicSuites.includes(matchingSuite.proofType) - } - ) - } - break - default: - throw new AriesFrameworkError( - `Unsupported requested credential format '${format}' with id ${ - credentialMetadata?.id ?? 'Inline credential offer' - }` - ) - } - - const supportsAllDidMethods = issuerSupportedBindingMethods?.includes('did') ?? false - const supportedDidMethods = issuerSupportedBindingMethods?.filter((method) => method.startsWith('did:')) - - if (!potentialSignatureAlgorithm) { - throw new AriesFrameworkError( - `Could not establish signature algorithm for format ${format} and id ${ - credentialMetadata?.id ?? 'Inline credential offer' - }` - ) - } - - return { - signatureAlgorithm: potentialSignatureAlgorithm, - supportedDidMethods, - supportsAllDidMethods, - } - } - - /** - * Returns the JWA Signature Algorithms that are supported by the wallet. - * - * This is an approximation based on the supported key types of the wallet. - * This is not 100% correct as a supporting a key type does not mean you support - * all the algorithms for that key type. However, this needs refactoring of the wallet - * that is planned for the 0.5.0 release. - */ - private getSupportedJwaSignatureAlgorithms(agentContext: AgentContext): JwaSignatureAlgorithm[] { - const supportedKeyTypes = agentContext.wallet.supportedKeyTypes - - // Extract the supported JWS algs based on the key types the wallet support. - const supportedJwaSignatureAlgorithms = supportedKeyTypes - // Map the supported key types to the supported JWK class - .map(getJwkClassFromKeyType) - // Filter out the undefined values - .filter((jwkClass): jwkClass is Exclude => jwkClass !== undefined) - // Extract the supported JWA signature algorithms from the JWK class - .map((jwkClass) => jwkClass.supportedSignatureAlgorithms) - // Flatten the array of arrays - .reduce((allAlgorithms, algorithms) => [...allAlgorithms, ...algorithms], []) - - return supportedJwaSignatureAlgorithms - } - - private async handleCredentialResponse( - agentContext: AgentContext, - credentialResponse: OpenIDResponse, - options: { verifyCredentialStatus: boolean } - ) { - this.logger.debug('Credential request response', credentialResponse) - - if (!credentialResponse.successBody) { - throw new AriesFrameworkError('Did not receive a successful credential response') - } - - const format = getUniformFormat(credentialResponse.successBody.format) - const difClaimFormat = fromOpenIdCredentialFormatProfileToDifClaimFormat(format as OpenIdCredentialFormatProfile) - - let credential: W3cVerifiableCredential - let result: W3cVerifyCredentialResult - if (difClaimFormat === ClaimFormat.LdpVc) { - credential = JsonTransformer.fromJSON(credentialResponse.successBody.credential, W3cJsonLdVerifiableCredential) - result = await this.w3cCredentialService.verifyCredential(agentContext, { - credential, - verifyCredentialStatus: options.verifyCredentialStatus, - }) - } else if (difClaimFormat === ClaimFormat.JwtVc) { - credential = W3cJwtVerifiableCredential.fromSerializedJwt(credentialResponse.successBody.credential as string) - result = await this.w3cCredentialService.verifyCredential(agentContext, { - credential, - verifyCredentialStatus: options.verifyCredentialStatus, - }) - } else { - throw new AriesFrameworkError(`Unsupported credential format ${credentialResponse.successBody.format}`) - } - - if (!result || !result.isValid) { - agentContext.config.logger.error('Failed to validate credential', { - result, - }) - throw new AriesFrameworkError(`Failed to validate credential, error = ${result.error?.message ?? 'Unknown'}`) - } - - return credential - } - - private signCallback(agentContext: AgentContext, verificationMethod: VerificationMethod) { - return async (jwt: Jwt, kid?: string) => { - if (!jwt.header) { - throw new AriesFrameworkError('No header present on JWT') - } - - if (!jwt.payload) { - throw new AriesFrameworkError('No payload present on JWT') - } - - if (!kid) { - throw new AriesFrameworkError('No KID is present in the callback') - } - - // We have determined the verification method before and already passed that when creating the callback, - // however we just want to make sure that the kid matches the verification method id - if (verificationMethod.id !== kid) { - throw new AriesFrameworkError(`kid ${kid} does not match verification method id ${verificationMethod.id}`) - } - - const key = getKeyFromVerificationMethod(verificationMethod) - const jwk = getJwkFromKey(key) - - const payload = JsonEncoder.toBuffer(jwt.payload) - if (!jwk.supportsSignatureAlgorithm(jwt.header.alg)) { - throw new AriesFrameworkError( - `kid ${kid} refers to a key of type '${jwk.keyType}', which does not support the JWS signature alg '${jwt.header.alg}'` - ) - } - - // We don't support these properties, remove them, so we can pass all other header properties to the JWS service - if (jwt.header.x5c || jwt.header.jwk) throw new AriesFrameworkError('x5c and jwk are not supported') - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { x5c: _x5c, jwk: _jwk, ...supportedHeaderOptions } = jwt.header - - const jws = await this.jwsService.createJwsCompact(agentContext, { - key, - payload, - protectedHeaderOptions: supportedHeaderOptions, - }) - - return jws - } - } -} - -function isInlineCredentialOffer(offeredCredential: OfferedCredentialsWithMetadata): offeredCredential is { - inlineCredentialOffer: CredentialOfferFormat - type: OfferedCredentialType.InlineCredentialOffer -} { - return offeredCredential.type === OfferedCredentialType.InlineCredentialOffer -} diff --git a/packages/openid4vc-client/src/OpenId4VcClientServiceOptions.ts b/packages/openid4vc-client/src/OpenId4VcClientServiceOptions.ts deleted file mode 100644 index ed03c68765..0000000000 --- a/packages/openid4vc-client/src/OpenId4VcClientServiceOptions.ts +++ /dev/null @@ -1,185 +0,0 @@ -import type { JwaSignatureAlgorithm, KeyType, VerificationMethod } from '@aries-framework/core' - -import { OpenIdCredentialFormatProfile } from './utils/claimFormatMapping' - -/** - * The credential formats that are supported by the openid4vc client - */ -export type SupportedCredentialFormats = OpenIdCredentialFormatProfile.JwtVcJson | OpenIdCredentialFormatProfile.LdpVc - -export const supportedCredentialFormats = [ - OpenIdCredentialFormatProfile.JwtVcJson, - OpenIdCredentialFormatProfile.LdpVc, -] satisfies OpenIdCredentialFormatProfile[] - -/** - * Options that are used for the pre-authorized code flow. - */ -export interface PreAuthCodeFlowOptions { - issuerUri: string - verifyCredentialStatus: boolean - - /** - * A list of allowed credential formats in order of preference. - * - * If the issuer supports one of the allowed formats, that first format that is supported - * from the list will be used. - * - * If the issuer doesn't support any of the allowed formats, an error is thrown - * and the request is aborted. - */ - allowedCredentialFormats?: SupportedCredentialFormats[] - - /** - * A list of allowed proof of possession signature algorithms in order of preference. - * - * Note that the signature algorithms must be supported by the wallet implementation. - * Signature algorithms that are not supported by the wallet will be ignored. - * - * The proof of possession (pop) signature algorithm is used in the credential request - * to bind the credential to a did. In most cases the JWA signature algorithm - * that is used in the pop will determine the cryptographic suite that is used - * for signing the credential, but this not a requirement for the spec. E.g. if the - * pop uses EdDsa, the credential will most commonly also use EdDsa, or Ed25519Signature2018/2020. - */ - allowedProofOfPossessionSignatureAlgorithms?: JwaSignatureAlgorithm[] - - /** - * A function that should resolve a verification method based on the options passed. - * This method will be called once for each of the credentials that are included - * in the credential offer. - * - * Based on the credential format, JWA signature algorithm, verification method types - * and did methods, the resolver must return a verification method that will be used - * for the proof of possession signature. - */ - proofOfPossessionVerificationMethodResolver: ProofOfPossessionVerificationMethodResolver -} - -/** - * Options that are used for the authorization code flow. - * Extends the pre-authorized code flow options. - */ -export interface AuthCodeFlowOptions extends PreAuthCodeFlowOptions { - clientId: string - authorizationCode: string - codeVerifier: string - redirectUri: string -} - -/** - * The options that are used to generate the authorization url. - * - * NOTE: The `code_challenge` property is omitted here - * because we assume it will always be SHA256 - * as clear text code challenges are unsafe. - */ -export interface GenerateAuthorizationUrlOptions { - initiationUri: string - clientId: string - redirectUri: string - scope?: string[] -} - -export interface ProofOfPossessionVerificationMethodResolverOptions { - /** - * The credential format that will be requested from the issuer. - * E.g. `jwt_vc` or `ldp_vc`. - */ - credentialFormat: SupportedCredentialFormats - - /** - * The JWA Signature Algorithm that will be used in the proof of possession. - * This is based on the `allowedProofOfPossessionSignatureAlgorithms` passed - * to the request credential method, and the supported signature algorithms. - */ - proofOfPossessionSignatureAlgorithm: JwaSignatureAlgorithm - - /** - * This is a list of verification methods types that are supported - * for creating the proof of possession signature. The returned - * verification method type must be of one of these types. - */ - supportedVerificationMethods: string[] - - /** - * The key type that will be used to create the proof of possession signature. - * This is related to the verification method and the signature algorithm, and - * is added for convenience. - */ - keyType: KeyType - - /** - * The credential type that will be requested from the issuer. This is - * based on the credential types that are included the credential offer. - * - * If the offered credential is an inline credential offer, the value - * will be `undefined`. - */ - // TODO: do we need credentialType here? - supportedCredentialId?: string - - /** - * Whether the issuer supports the `did` cryptographic binding method, - * indicating they support all did methods. In most cases, they do not - * support all did methods, and it means we have to make an assumption - * about the did methods they support. - * - * If this value is `false`, the `supportedDidMethods` property will - * contain a list of supported did methods. - */ - supportsAllDidMethods: boolean - - /** - * A list of supported did methods. This is only used if the `supportsAllDidMethods` - * property is `false`. When this array is populated, the returned verification method - * MUST be based on one of these did methods. - * - * The did methods are returned in the format `did:`, e.g. `did:web`. - * - * The value is undefined in the case the supported did methods could not be extracted. - * This is the case when an inline credential was used, or when the issuer didn't include - * the supported did methods in the issuer metadata. - * - * NOTE: an empty array (no did methods supported) has a different meaning from the value - * being undefined (the supported did methods could not be extracted). If `supportsAllDidMethods` - * is true, the value of this property MUST be ignored. - */ - supportedDidMethods?: string[] -} - -/** - * The proof of possession verification method resolver is a function that can be passed by the - * user of the framework and allows them to determine which verification method should be used - * for the proof of possession signature. - */ -export type ProofOfPossessionVerificationMethodResolver = ( - options: ProofOfPossessionVerificationMethodResolverOptions -) => Promise | VerificationMethod - -/** - * @internal - */ -export interface ProofOfPossessionRequirements { - signatureAlgorithm: JwaSignatureAlgorithm - supportedDidMethods?: string[] - supportsAllDidMethods: boolean -} - -/** - * @internal - */ -export enum AuthFlowType { - AuthorizationCodeFlow, - PreAuthorizedCodeFlow, -} - -type WithFlowType = Options & { flowType: FlowType } - -/** - * The options that are used to request a credential from an issuer. - * @internal - */ -export type RequestCredentialOptions = - | WithFlowType - | WithFlowType diff --git a/packages/openid4vc-client/src/index.ts b/packages/openid4vc-client/src/index.ts deleted file mode 100644 index f6e1e75c5d..0000000000 --- a/packages/openid4vc-client/src/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -import 'fast-text-encoding' - -export * from './OpenId4VcClientApi' -export * from './OpenId4VcClientModule' -export * from './OpenId4VcClientService' -// Contains internal types, so we don't export everything -export { - AuthCodeFlowOptions, - GenerateAuthorizationUrlOptions, - PreAuthCodeFlowOptions, - ProofOfPossessionVerificationMethodResolver, - ProofOfPossessionVerificationMethodResolverOptions, - RequestCredentialOptions, - SupportedCredentialFormats, -} from './OpenId4VcClientServiceOptions' -export * from './presentations' -export { - getOpenId4VcCredentialMetadata, - OpenId4VcCredentialMetadata, - OpenIdCredentialFormatProfile, - setOpenId4VcCredentialMetadata, -} from './utils' diff --git a/packages/openid4vc-client/src/utils/Formats.ts b/packages/openid4vc-client/src/utils/Formats.ts deleted file mode 100644 index 7d13fa6ef9..0000000000 --- a/packages/openid4vc-client/src/utils/Formats.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { OID4VCICredentialFormat } from '@sphereon/oid4vci-common' -import type { CredentialFormat } from '@sphereon/ssi-types' - -import { OpenId4VCIVersion } from '@sphereon/oid4vci-common' - -// Base on https://github.com/Sphereon-Opensource/OID4VCI/pull/54/files - -const isUniformFormat = (format: string): format is OID4VCICredentialFormat => { - return ['jwt_vc_json', 'jwt_vc_json-ld', 'ldp_vc'].includes(format) -} - -export function getUniformFormat(format: string | OID4VCICredentialFormat | CredentialFormat): OID4VCICredentialFormat { - // Already valid format - if (isUniformFormat(format)) { - return format - } - - // Older formats - if (format === 'jwt_vc' || format === 'jwt') { - return 'jwt_vc_json' - } - if (format === 'ldp_vc' || format === 'ldp') { - return 'ldp_vc' - } - - throw new Error(`Invalid format: ${format}`) -} - -export function getFormatForVersion(format: string, version: OpenId4VCIVersion) { - const uniformFormat = isUniformFormat(format) ? format : getUniformFormat(format) - - if (version < OpenId4VCIVersion.VER_1_0_11) { - if (uniformFormat === 'jwt_vc_json') { - return 'jwt_vc' as const - } else if (uniformFormat === 'ldp_vc' || uniformFormat === 'jwt_vc_json-ld') { - return 'ldp_vc' as const - } - } - - return uniformFormat -} diff --git a/packages/openid4vc-client/src/utils/IssuerMetadataUtils.ts b/packages/openid4vc-client/src/utils/IssuerMetadataUtils.ts deleted file mode 100644 index 827cfdaa5e..0000000000 --- a/packages/openid4vc-client/src/utils/IssuerMetadataUtils.ts +++ /dev/null @@ -1,113 +0,0 @@ -import type { - CredentialIssuerMetadata, - CredentialSupported, - CredentialSupportedTypeV1_0_08, - CredentialSupportedV1_0_08, - IssuerMetadataV1_0_08, - MetadataDisplay, -} from '@sphereon/oid4vci-common' - -import { OpenId4VCIVersion } from '@sphereon/oid4vci-common' - -export function getSupportedCredentials(opts?: { - issuerMetadata?: CredentialIssuerMetadata | IssuerMetadataV1_0_08 - version: OpenId4VCIVersion - credentialSupportedIds?: string[] -}): CredentialSupported[] { - const { issuerMetadata } = opts ?? {} - let credentialsSupported: CredentialSupported[] - if (!issuerMetadata) { - return [] - } - const { version, credentialSupportedIds } = opts ?? { version: OpenId4VCIVersion.VER_1_0_11 } - - const usesTransformedCredentialsSupported = - version === OpenId4VCIVersion.VER_1_0_08 || !Array.isArray(issuerMetadata.credentials_supported) - if (usesTransformedCredentialsSupported) { - credentialsSupported = credentialsSupportedV8ToV11((issuerMetadata as IssuerMetadataV1_0_08).credentials_supported) - } else { - credentialsSupported = (issuerMetadata as CredentialIssuerMetadata).credentials_supported - } - - if (credentialsSupported === undefined || credentialsSupported.length === 0) { - return [] - } else if (!credentialSupportedIds || credentialSupportedIds.length === 0) { - return credentialsSupported - } - - const credentialSupportedOverlap: CredentialSupported[] = [] - for (const credentialSupportedId of credentialSupportedIds) { - if (typeof credentialSupportedId === 'string') { - const supported = credentialsSupported.find((sup) => { - // Match id to offerType - if (sup.id === credentialSupportedId) return true - - // If the credential was transformed and the v8 variant supported multiple formats for the id, we - // check if there is an id with the format - // see credentialsSupportedV8ToV11 - if (usesTransformedCredentialsSupported && sup.id === `${credentialSupportedId}-${sup.format}`) return true - - return false - }) - if (supported) { - credentialSupportedOverlap.push(supported) - } - } - } - - return credentialSupportedOverlap -} - -export function credentialsSupportedV8ToV11(supportedV8: CredentialSupportedTypeV1_0_08): CredentialSupported[] { - return Object.entries(supportedV8).flatMap((entry) => { - const type = entry[0] - const supportedV8 = entry[1] - return credentialSupportedV8ToV11(type, supportedV8) - }) -} - -export function credentialSupportedV8ToV11( - key: string, - supportedV8: CredentialSupportedV1_0_08 -): CredentialSupported[] { - const v8FormatEntries = Object.entries(supportedV8.formats) - - return v8FormatEntries.map((entry) => { - const format = entry[0] - const credentialSupportBrief = entry[1] - if (typeof format !== 'string') { - throw Error(`Unknown format received ${JSON.stringify(format)}`) - } - let credentialSupport: Partial = {} - - // v8 format included the credential type / id as the key of the object and it could contain multiple supported formats - // v11 format has an array where each entry only supports one format, and can only have an `id` property. We include the - // key from the v8 object as the id for the v11 object, but to prevent collisions (as multiple formats can be supported under - // one key), we append the format to the key IF there's more than one format supported under the key. - const id = v8FormatEntries.length > 1 ? `${key}-${format}` : key - - credentialSupport = { - format, - display: supportedV8.display, - ...credentialSupportBrief, - credentialSubject: supportedV8.claims, - id, - } - return credentialSupport as CredentialSupported - }) -} - -export function getIssuerDisplays( - metadata: CredentialIssuerMetadata | IssuerMetadataV1_0_08, - opts?: { prefLocales: string[] } -): MetadataDisplay[] { - const matchedDisplays = - metadata.display?.filter( - (item) => - !opts?.prefLocales || - opts.prefLocales.length === 0 || - (item.locale && opts.prefLocales.includes(item.locale)) || - !item.locale - ) ?? [] - return matchedDisplays.sort((item) => (item.locale ? opts?.prefLocales.indexOf(item.locale) ?? 1 : Number.MAX_VALUE)) -} diff --git a/packages/openid4vc-client/src/utils/__tests__/claimFormatMapping.test.ts b/packages/openid4vc-client/src/utils/__tests__/claimFormatMapping.test.ts deleted file mode 100644 index a8bcdd9633..0000000000 --- a/packages/openid4vc-client/src/utils/__tests__/claimFormatMapping.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { AriesFrameworkError, ClaimFormat } from '@aries-framework/core' - -import { - fromDifClaimFormatToOpenIdCredentialFormatProfile, - fromOpenIdCredentialFormatProfileToDifClaimFormat, - OpenIdCredentialFormatProfile, -} from '../claimFormatMapping' - -describe('claimFormatMapping', () => { - it('should convert from openid credential format profile to DIF claim format', () => { - expect(fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.LdpVc)).toStrictEqual( - OpenIdCredentialFormatProfile.LdpVc - ) - - expect(fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.JwtVc)).toStrictEqual( - OpenIdCredentialFormatProfile.JwtVcJson - ) - - expect(() => fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.Jwt)).toThrow(AriesFrameworkError) - - expect(() => fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.Ldp)).toThrow(AriesFrameworkError) - - expect(() => fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.JwtVp)).toThrow(AriesFrameworkError) - - expect(() => fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.LdpVp)).toThrow(AriesFrameworkError) - }) - - it('should convert from DIF claim format to openid credential format profile', () => { - expect(fromOpenIdCredentialFormatProfileToDifClaimFormat(OpenIdCredentialFormatProfile.JwtVcJson)).toStrictEqual( - ClaimFormat.JwtVc - ) - - expect(fromOpenIdCredentialFormatProfileToDifClaimFormat(OpenIdCredentialFormatProfile.JwtVcJsonLd)).toStrictEqual( - ClaimFormat.JwtVc - ) - - expect(fromOpenIdCredentialFormatProfileToDifClaimFormat(OpenIdCredentialFormatProfile.LdpVc)).toStrictEqual( - ClaimFormat.LdpVc - ) - - expect(() => fromOpenIdCredentialFormatProfileToDifClaimFormat(OpenIdCredentialFormatProfile.MsoMdoc)).toThrow( - AriesFrameworkError - ) - }) -}) diff --git a/packages/openid4vc-client/src/utils/claimFormatMapping.ts b/packages/openid4vc-client/src/utils/claimFormatMapping.ts deleted file mode 100644 index 3ab952f94f..0000000000 --- a/packages/openid4vc-client/src/utils/claimFormatMapping.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { AriesFrameworkError, ClaimFormat } from '@aries-framework/core' - -export enum OpenIdCredentialFormatProfile { - JwtVcJson = 'jwt_vc_json', - JwtVcJsonLd = 'jwt_vc_json-ld', - LdpVc = 'ldp_vc', - MsoMdoc = 'mso_mdoc', -} - -export const fromDifClaimFormatToOpenIdCredentialFormatProfile = ( - claimFormat: ClaimFormat -): OpenIdCredentialFormatProfile => { - switch (claimFormat) { - case ClaimFormat.JwtVc: - return OpenIdCredentialFormatProfile.JwtVcJson - case ClaimFormat.LdpVc: - return OpenIdCredentialFormatProfile.LdpVc - default: - throw new AriesFrameworkError( - `Unsupported DIF claim format, ${claimFormat}, to map to an openid credential format profile` - ) - } -} - -export const fromOpenIdCredentialFormatProfileToDifClaimFormat = ( - openidCredentialFormatProfile: OpenIdCredentialFormatProfile -): ClaimFormat => { - switch (openidCredentialFormatProfile) { - case OpenIdCredentialFormatProfile.JwtVcJson: - return ClaimFormat.JwtVc - case OpenIdCredentialFormatProfile.JwtVcJsonLd: - return ClaimFormat.JwtVc - case OpenIdCredentialFormatProfile.LdpVc: - return ClaimFormat.LdpVc - default: - throw new AriesFrameworkError( - `Unsupported openid credential format profile, ${openidCredentialFormatProfile}, to map to a DIF claim format` - ) - } -} diff --git a/packages/openid4vc-client/src/utils/index.ts b/packages/openid4vc-client/src/utils/index.ts deleted file mode 100644 index ee55f476ed..0000000000 --- a/packages/openid4vc-client/src/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './claimFormatMapping' -export * from './metadata' diff --git a/packages/openid4vc-client/src/utils/metadata.ts b/packages/openid4vc-client/src/utils/metadata.ts deleted file mode 100644 index 2ae316632b..0000000000 --- a/packages/openid4vc-client/src/utils/metadata.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { W3cCredentialRecord } from '@aries-framework/core' -import type { - CredentialIssuerMetadata, - CredentialsSupportedDisplay, - CredentialSupported, - EndpointMetadata, - IssuerCredentialSubject, - IssuerMetadataV1_0_08, - MetadataDisplay, -} from '@sphereon/oid4vci-common' - -export interface OpenId4VcCredentialMetadata { - credential: { - display?: CredentialsSupportedDisplay[] - order?: string[] - credentialSubject: IssuerCredentialSubject - } - issuer: { - display?: MetadataDisplay[] - id: string - } -} - -// what does this mean -const openId4VcCredentialMetadataKey = '_paradym/openId4VcCredentialMetadata' - -function extractOpenId4VcCredentialMetadata( - credentialMetadata: CredentialSupported, - serverMetadata: EndpointMetadata, - serverMetadataResult: CredentialIssuerMetadata | IssuerMetadataV1_0_08 -) { - return { - credential: { - display: credentialMetadata.display, - order: credentialMetadata.order, - credentialSubject: credentialMetadata.credentialSubject, - }, - issuer: { - display: serverMetadataResult.credentialIssuerMetadata?.display, - id: serverMetadata.issuer, - }, - } -} - -/** - * Gets the OpenId4Vc credential metadata from the given W3C credential record. - */ -export function getOpenId4VcCredentialMetadata( - w3cCredentialRecord: W3cCredentialRecord -): OpenId4VcCredentialMetadata | null { - return w3cCredentialRecord.metadata.get(openId4VcCredentialMetadataKey) -} - -/** - * Sets the OpenId4Vc credential metadata on the given W3C credential record. - * - * NOTE: this does not save the record. - */ -export function setOpenId4VcCredentialMetadata( - w3cCredentialRecord: W3cCredentialRecord, - credentialMetadata: CredentialSupported, - serverMetadata: EndpointMetadata, - serverMetadataResult: CredentialIssuerMetadata | IssuerMetadataV1_0_08 -) { - w3cCredentialRecord.metadata.set( - openId4VcCredentialMetadataKey, - extractOpenId4VcCredentialMetadata(credentialMetadata, serverMetadata, serverMetadataResult) - ) -} diff --git a/packages/openid4vc-client/tests/OpenId4VcClientModule.test.ts b/packages/openid4vc-client/tests/OpenId4VcClientModule.test.ts deleted file mode 100644 index b3383d8d6c..0000000000 --- a/packages/openid4vc-client/tests/OpenId4VcClientModule.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* eslint-disable @typescript-eslint/unbound-method */ -import type { DependencyManager } from '@aries-framework/core' - -import { OpenId4VcClientApi } from '../src/OpenId4VcClientApi' -import { OpenId4VcClientModule } from '../src/OpenId4VcClientModule' -import { OpenId4VcClientService } from '../src/OpenId4VcClientService' -import { OpenId4VpClientService, PresentationExchangeService } from '../src/presentations' - -const dependencyManager = { - registerInstance: jest.fn(), - registerSingleton: jest.fn(), - registerContextScoped: jest.fn(), - resolve: jest.fn().mockReturnValue({ logger: { warn: jest.fn() } }), -} as unknown as DependencyManager - -describe('OpenId4VcClientModule', () => { - test('registers dependencies on the dependency manager', () => { - const openId4VcClientModule = new OpenId4VcClientModule() - openId4VcClientModule.register(dependencyManager) - - expect(dependencyManager.registerContextScoped).toHaveBeenCalledTimes(1) - expect(dependencyManager.registerContextScoped).toHaveBeenCalledWith(OpenId4VcClientApi) - - expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(3) - expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VcClientService) - expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VpClientService) - expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(PresentationExchangeService) - }) -}) diff --git a/packages/openid4vc-client/tests/fixtures.ts b/packages/openid4vc-client/tests/fixtures.ts deleted file mode 100644 index 54e46bb496..0000000000 --- a/packages/openid4vc-client/tests/fixtures.ts +++ /dev/null @@ -1,460 +0,0 @@ -export const mattrLaunchpadJsonLd_draft_08 = { - credentialOffer: - 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=OpenBadgeCredential&pre-authorized_code=krBcsBIlye2T-G4-rHHnRZUCah9uzDKwohJK6ABNvL-', - getMetadataResponse: { - authorization_endpoint: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/authorize', - token_endpoint: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/token', - jwks_uri: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/jwks', - token_endpoint_auth_methods_supported: [ - 'none', - 'client_secret_basic', - 'client_secret_jwt', - 'client_secret_post', - 'private_key_jwt', - ], - code_challenge_methods_supported: ['S256'], - grant_types_supported: ['authorization_code', 'urn:ietf:params:oauth:grant-type:pre-authorized_code'], - response_modes_supported: ['form_post', 'fragment', 'query'], - response_types_supported: ['code id_token', 'code', 'id_token', 'none'], - scopes_supported: ['OpenBadgeCredential', 'AcademicAward', 'LearnerProfile', 'PermanentResidentCard'], - token_endpoint_auth_signing_alg_values_supported: ['HS256', 'RS256', 'PS256', 'ES256', 'EdDSA'], - credential_endpoint: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/credential', - credentials_supported: { - OpenBadgeCredential: { - formats: { - ldp_vc: { - name: 'JFF x vc-edu PlugFest 2', - description: "MATTR's submission for JFF Plugfest 2", - types: ['OpenBadgeCredential'], - binding_methods_supported: ['did'], - cryptographic_suites_supported: ['Ed25519Signature2018'], - }, - }, - }, - AcademicAward: { - formats: { - ldp_vc: { - name: 'Example Academic Award', - description: 'Microcredential from the MyCreds Network.', - types: ['AcademicAward'], - binding_methods_supported: ['did'], - cryptographic_suites_supported: ['Ed25519Signature2018'], - }, - }, - }, - LearnerProfile: { - formats: { - ldp_vc: { - name: 'Digitary Learner Profile', - description: 'Example', - types: ['LearnerProfile'], - binding_methods_supported: ['did'], - cryptographic_suites_supported: ['Ed25519Signature2018'], - }, - }, - }, - PermanentResidentCard: { - formats: { - ldp_vc: { - name: 'Permanent Resident Card', - description: 'Government of Kakapo', - types: ['PermanentResidentCard'], - binding_methods_supported: ['did'], - cryptographic_suites_supported: ['Ed25519Signature2018'], - }, - }, - }, - }, - }, - - acquireAccessTokenResponse: { - access_token: '7nikUotMQefxn7oRX56R7MDNE7KJTGfwGjOkHzGaUIG', - expires_in: 3600, - scope: 'OpenBadgeCredential', - token_type: 'Bearer', - }, - credentialResponse: { - format: 'ldp_vc', - credential: { - type: ['VerifiableCredential', 'VerifiableCredentialExtension', 'OpenBadgeCredential'], - issuer: { - id: 'did:web:launchpad.vii.electron.mattrlabs.io', - name: 'Jobs for the Future (JFF)', - iconUrl: 'https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png', - image: 'https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png', - }, - name: 'JFF x vc-edu PlugFest 2', - description: "MATTR's submission for JFF Plugfest 2", - credentialBranding: { - backgroundColor: '#464c49', - }, - issuanceDate: '2023-01-25T16:58:06.292Z', - credentialSubject: { - id: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', - type: ['AchievementSubject'], - achievement: { - id: 'urn:uuid:bd6d9316-f7ae-4073-a1e5-2f7f5bd22922', - name: 'JFF x vc-edu PlugFest 2 Interoperability', - type: ['Achievement'], - image: { - id: 'https://w3c-ccg.github.io/vc-ed/plugfest-2-2022/images/JFF-VC-EDU-PLUGFEST2-badge-image.png', - type: 'Image', - }, - criteria: { - type: 'Criteria', - narrative: - 'Solutions providers earned this badge by demonstrating interoperability between multiple providers based on the OBv3 candidate final standard, with some additional required fields. Credential issuers earning this badge successfully issued a credential into at least two wallets. Wallet implementers earning this badge successfully displayed credentials issued by at least two different credential issuers.', - }, - description: - 'This credential solution supports the use of OBv3 and w3c Verifiable Credentials and is interoperable with at least two other solutions. This was demonstrated successfully during JFF x vc-edu PlugFest 2.', - }, - }, - '@context': [ - 'https://www.w3.org/2018/credentials/v1', - { - '@vocab': 'https://w3id.org/security/undefinedTerm#', - }, - 'https://mattr.global/contexts/vc-extensions/v1', - 'https://purl.imsglobal.org/spec/ob/v3p0/context.json', - 'https://w3c-ccg.github.io/vc-status-rl-2020/contexts/vc-revocation-list-2020/v1.jsonld', - ], - credentialStatus: { - id: 'https://launchpad.vii.electron.mattrlabs.io/core/v1/revocation-lists/b4aa46a0-5539-4a6b-aa03-8f6791c22ce3#49', - type: 'RevocationList2020Status', - revocationListIndex: '49', - revocationListCredential: - 'https://launchpad.vii.electron.mattrlabs.io/core/v1/revocation-lists/b4aa46a0-5539-4a6b-aa03-8f6791c22ce3', - }, - proof: { - type: 'Ed25519Signature2018', - created: '2023-01-25T16:58:07Z', - jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..PrpRKt60yXOzMNiQY5bELX40F6Svwm-FyQ-Jv02VJDfTTH8GPPByjtOb_n3YfWidQVgySfGQ_H7VmCGjvsU6Aw', - proofPurpose: 'assertionMethod', - verificationMethod: 'did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg', - }, - }, - }, -} - -export const waltIdJffJwt_draft_08 = { - credentialOffer: - 'openid-initiate-issuance://?issuer=https%3A%2F%2Fjff.walt.id%2Fissuer-api%2Fdefault%2Foidc%2F&credential_type=VerifiableId&pre-authorized_code=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI4YmI0NWZiNC0zNDc1LTQ5YzItODVjNy0wYjkxZjY4N2RhNDQiLCJwcmUtYXV0aG9yaXplZCI6dHJ1ZX0.R8nHseZJvU3uVL3Ox-97i1HUnvjZH6wKSWDO_i8D12I&user_pin_required=false', - getMetadataResponse: { - authorization_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/fulfillPAR', - token_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/token', - pushed_authorization_request_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/par', - issuer: 'https://jff.walt.id/issuer-api/default', - jwks_uri: 'https://jff.walt.id/issuer-api/default/oidc', - grant_types_supported: ['authorization_code', 'urn:ietf:params:oauth:grant-type:pre-authorized_code'], - request_uri_parameter_supported: true, - credentials_supported: { - VerifiableId: { - display: [{ name: 'VerifiableId' }], - formats: { - ldp_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableId'], - }, - jwt_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], - types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableId'], - }, - }, - }, - VerifiableDiploma: { - display: [{ name: 'VerifiableDiploma' }], - formats: { - ldp_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableDiploma'], - }, - jwt_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], - types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableDiploma'], - }, - }, - }, - VerifiableVaccinationCertificate: { - display: [{ name: 'VerifiableVaccinationCertificate' }], - formats: { - ldp_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableVaccinationCertificate'], - }, - jwt_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], - types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableVaccinationCertificate'], - }, - }, - }, - ProofOfResidence: { - display: [{ name: 'ProofOfResidence' }], - formats: { - ldp_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'VerifiableAttestation', 'ProofOfResidence'], - }, - jwt_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], - types: ['VerifiableCredential', 'VerifiableAttestation', 'ProofOfResidence'], - }, - }, - }, - ParticipantCredential: { - display: [{ name: 'ParticipantCredential' }], - formats: { - ldp_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'ParticipantCredential'], - }, - jwt_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], - types: ['VerifiableCredential', 'ParticipantCredential'], - }, - }, - }, - Europass: { - display: [{ name: 'Europass' }], - formats: { - ldp_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'VerifiableAttestation', 'Europass'], - }, - jwt_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], - types: ['VerifiableCredential', 'VerifiableAttestation', 'Europass'], - }, - }, - }, - OpenBadgeCredential: { - display: [{ name: 'OpenBadgeCredential' }], - formats: { - ldp_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'OpenBadgeCredential'], - }, - jwt_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], - types: ['VerifiableCredential', 'OpenBadgeCredential'], - }, - }, - }, - }, - credential_issuer: { - display: [{ locale: null, name: 'https://jff.walt.id/issuer-api/default' }], - }, - credential_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/credential', - subject_types_supported: ['public'], - }, - - acquireAccessTokenResponse: { - access_token: '8bb45fb4-3475-49c2-85c7-0b91f687da44', - refresh_token: 'WEjORX8NZccRGtRN4yvXFdYE8MeAOaLLmmGlcRbutq4', - c_nonce: 'cbad6376-f882-44c5-ae88-19bccc0de124', - id_token: - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI4YmI0NWZiNC0zNDc1LTQ5YzItODVjNy0wYjkxZjY4N2RhNDQifQ.Mca0Ln1AvNlxBJftYc1PZKQBlGdBmrHsFRQSBDoCgD0', - token_type: 'Bearer', - expires_in: 300, - }, - - credentialResponse: { - credential: - 'eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCMwIiwidHlwIjoiSldUIiwiYWxnIjoiRWREU0EifQ.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCIsInN1YiI6ImRpZDprZXk6ekRuYWVpcFdnOURNWFB0OWpjbUFCcWFZUlZLYzE5dFgxeGZCUldGc0pTUG9VZE1udiIsIm5iZiI6MTY4NTM1MDc4OSwiaWF0IjoxNjg1MzUwNzg5LCJ2YyI6eyJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVmVyaWZpYWJsZUF0dGVzdGF0aW9uIiwiVmVyaWZpYWJsZUlkIl0sIkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sImlkIjoidXJuOnV1aWQ6NTljZTRhYzItZWM2NS00YjhmLThmOTYtZWE3ODUxMmRmOWQzIiwiaXNzdWVyIjoiZGlkOmp3azpleUpyZEhraU9pSlBTMUFpTENKMWMyVWlPaUp6YVdjaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWEybGtJam9pTjJRMlkySm1NalE0T1dJek5ESTNObUl4TnpJeE9UQTFORGxrTWpNNU1UZ2lMQ0o0SWpvaVJtNUZWVlZoZFdSdE9UbE9NekJpT0RCcWN6aFdkRFJCYms5NGRsSjNXSFJuVW1OTGNUTm5Ra2wxT0NJc0ltRnNaeUk2SWtWa1JGTkJJbjAiLCJpc3N1YW5jZURhdGUiOiIyMDIzLTA1LTI5VDA4OjU5OjQ5WiIsImlzc3VlZCI6IjIwMjMtMDUtMjlUMDg6NTk6NDlaIiwidmFsaWRGcm9tIjoiMjAyMy0wNS0yOVQwODo1OTo0OVoiLCJjcmVkZW50aWFsU2NoZW1hIjp7ImlkIjoiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3dhbHQtaWQvd2FsdGlkLXNzaWtpdC12Y2xpYi9tYXN0ZXIvc3JjL3Rlc3QvcmVzb3VyY2VzL3NjaGVtYXMvVmVyaWZpYWJsZUlkLmpzb24iLCJ0eXBlIjoiRnVsbEpzb25TY2hlbWFWYWxpZGF0b3IyMDIxIn0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6RG5hZWlwV2c5RE1YUHQ5amNtQUJxYVlSVktjMTl0WDF4ZkJSV0ZzSlNQb1VkTW52IiwiY3VycmVudEFkZHJlc3MiOlsiMSBCb3VsZXZhcmQgZGUgbGEgTGliZXJ0w6ksIDU5ODAwIExpbGxlIl0sImRhdGVPZkJpcnRoIjoiMTk5My0wNC0wOCIsImZhbWlseU5hbWUiOiJET0UiLCJmaXJzdE5hbWUiOiJKYW5lIiwiZ2VuZGVyIjoiRkVNQUxFIiwibmFtZUFuZEZhbWlseU5hbWVBdEJpcnRoIjoiSmFuZSBET0UiLCJwZXJzb25hbElkZW50aWZpZXIiOiIwOTA0MDA4MDg0SCIsInBsYWNlT2ZCaXJ0aCI6IkxJTExFLCBGUkFOQ0UifSwiZXZpZGVuY2UiOlt7ImRvY3VtZW50UHJlc2VuY2UiOlsiUGh5c2ljYWwiXSwiZXZpZGVuY2VEb2N1bWVudCI6WyJQYXNzcG9ydCJdLCJzdWJqZWN0UHJlc2VuY2UiOiJQaHlzaWNhbCIsInR5cGUiOlsiRG9jdW1lbnRWZXJpZmljYXRpb24iXSwidmVyaWZpZXIiOiJkaWQ6ZWJzaToyQTlCWjlTVWU2QmF0YWNTcHZzMVY1Q2RqSHZMcFE3YkVzaTJKYjZMZEhLblF4YU4ifV19LCJqdGkiOiJ1cm46dXVpZDo1OWNlNGFjMi1lYzY1LTRiOGYtOGY5Ni1lYTc4NTEyZGY5ZDMifQ.6Wn8X2tEQJ9CmX3-meCxDuGmevRdtivnjVkGPXzfnJ-1M6AU4SFxxon0JmMjdmO_h4P9sCEe9RTtyTJou2yeCA', - format: 'jwt_vc', - }, -} - -// This object is MANUALLY converted and should be updated when we have actual test vectors -export const waltIdJffJwt_draft_11 = { - credentialOffer: - 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fjff.walt.id%2Fissuer-api%2Fdefault%2Foidc%22%2C%22credentials%22%3A%5B%22VerifiableId%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22ABC%22%7D%7D%7D', - getMetadataResponse: { - authorization_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/fulfillPAR', - token_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/token', - pushed_authorization_request_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/par', - credential_issuer: 'https://jff.walt.id/issuer-api/default', - jwks_uri: 'https://jff.walt.id/issuer-api/default/oidc', - credential_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/credential', - subject_types_supported: ['public'], - grant_types_supported: ['authorization_code', 'urn:ietf:params:oauth:grant-type:pre-authorized_code'], - request_uri_parameter_supported: true, - credentials_supported: [ - { - id: 'VerifiableId', - format: 'jwt_vc_json', - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], - types: ['VerifiableCredential', 'OpenBadgeCredential'], - }, - { - id: 'VerifiableDiploma', - display: [{ name: 'VerifiableDiploma' }], - format: 'ldp_vc', - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableDiploma'], - }, - { - id: 'VerifiableVaccinationCertificate', - display: [{ name: 'VerifiableVaccinationCertificate' }], - format: 'ldp_vc', - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableVaccinationCertificate'], - }, - { - id: 'ProofOfResidence', - display: [{ name: 'ProofOfResidence' }], - format: 'ldp_vc', - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'VerifiableAttestation', 'ProofOfResidence'], - }, - { - id: 'ParticipantCredential', - format: 'ldp_vc', - display: [{ name: 'ParticipantCredential' }], - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'ParticipantCredential'], - }, - { - id: 'Europass', - display: [{ name: 'Europass' }], - format: 'ldp_vc', - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'VerifiableAttestation', 'Europass'], - }, - { - id: 'OpenBadgeCredential', - display: [{ name: 'OpenBadgeCredential' }], - format: 'ldp_vc', - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'OpenBadgeCredential'], - }, - ], - }, - - acquireAccessTokenResponse: { - access_token: '8bb45fb4-3475-49c2-85c7-0b91f687da44', - refresh_token: 'WEjORX8NZccRGtRN4yvXFdYE8MeAOaLLmmGlcRbutq4', - c_nonce: 'cbad6376-f882-44c5-ae88-19bccc0de124', - id_token: - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI4YmI0NWZiNC0zNDc1LTQ5YzItODVjNy0wYjkxZjY4N2RhNDQifQ.Mca0Ln1AvNlxBJftYc1PZKQBlGdBmrHsFRQSBDoCgD0', - token_type: 'Bearer', - expires_in: 300, - }, - - credentialResponse: { - credential: - 'eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCMwIiwidHlwIjoiSldUIiwiYWxnIjoiRWREU0EifQ.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCIsInN1YiI6ImRpZDprZXk6ekRuYWVpcFdnOURNWFB0OWpjbUFCcWFZUlZLYzE5dFgxeGZCUldGc0pTUG9VZE1udiIsIm5iZiI6MTY4NTM1MDc4OSwiaWF0IjoxNjg1MzUwNzg5LCJ2YyI6eyJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVmVyaWZpYWJsZUF0dGVzdGF0aW9uIiwiVmVyaWZpYWJsZUlkIl0sIkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sImlkIjoidXJuOnV1aWQ6NTljZTRhYzItZWM2NS00YjhmLThmOTYtZWE3ODUxMmRmOWQzIiwiaXNzdWVyIjoiZGlkOmp3azpleUpyZEhraU9pSlBTMUFpTENKMWMyVWlPaUp6YVdjaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWEybGtJam9pTjJRMlkySm1NalE0T1dJek5ESTNObUl4TnpJeE9UQTFORGxrTWpNNU1UZ2lMQ0o0SWpvaVJtNUZWVlZoZFdSdE9UbE9NekJpT0RCcWN6aFdkRFJCYms5NGRsSjNXSFJuVW1OTGNUTm5Ra2wxT0NJc0ltRnNaeUk2SWtWa1JGTkJJbjAiLCJpc3N1YW5jZURhdGUiOiIyMDIzLTA1LTI5VDA4OjU5OjQ5WiIsImlzc3VlZCI6IjIwMjMtMDUtMjlUMDg6NTk6NDlaIiwidmFsaWRGcm9tIjoiMjAyMy0wNS0yOVQwODo1OTo0OVoiLCJjcmVkZW50aWFsU2NoZW1hIjp7ImlkIjoiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3dhbHQtaWQvd2FsdGlkLXNzaWtpdC12Y2xpYi9tYXN0ZXIvc3JjL3Rlc3QvcmVzb3VyY2VzL3NjaGVtYXMvVmVyaWZpYWJsZUlkLmpzb24iLCJ0eXBlIjoiRnVsbEpzb25TY2hlbWFWYWxpZGF0b3IyMDIxIn0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6RG5hZWlwV2c5RE1YUHQ5amNtQUJxYVlSVktjMTl0WDF4ZkJSV0ZzSlNQb1VkTW52IiwiY3VycmVudEFkZHJlc3MiOlsiMSBCb3VsZXZhcmQgZGUgbGEgTGliZXJ0w6ksIDU5ODAwIExpbGxlIl0sImRhdGVPZkJpcnRoIjoiMTk5My0wNC0wOCIsImZhbWlseU5hbWUiOiJET0UiLCJmaXJzdE5hbWUiOiJKYW5lIiwiZ2VuZGVyIjoiRkVNQUxFIiwibmFtZUFuZEZhbWlseU5hbWVBdEJpcnRoIjoiSmFuZSBET0UiLCJwZXJzb25hbElkZW50aWZpZXIiOiIwOTA0MDA4MDg0SCIsInBsYWNlT2ZCaXJ0aCI6IkxJTExFLCBGUkFOQ0UifSwiZXZpZGVuY2UiOlt7ImRvY3VtZW50UHJlc2VuY2UiOlsiUGh5c2ljYWwiXSwiZXZpZGVuY2VEb2N1bWVudCI6WyJQYXNzcG9ydCJdLCJzdWJqZWN0UHJlc2VuY2UiOiJQaHlzaWNhbCIsInR5cGUiOlsiRG9jdW1lbnRWZXJpZmljYXRpb24iXSwidmVyaWZpZXIiOiJkaWQ6ZWJzaToyQTlCWjlTVWU2QmF0YWNTcHZzMVY1Q2RqSHZMcFE3YkVzaTJKYjZMZEhLblF4YU4ifV19LCJqdGkiOiJ1cm46dXVpZDo1OWNlNGFjMi1lYzY1LTRiOGYtOGY5Ni1lYTc4NTEyZGY5ZDMifQ.6Wn8X2tEQJ9CmX3-meCxDuGmevRdtivnjVkGPXzfnJ-1M6AU4SFxxon0JmMjdmO_h4P9sCEe9RTtyTJou2yeCA', - format: 'jwt_vc', - }, -} diff --git a/packages/openid4vc-client/tests/openid4vc-client.e2e.test.ts b/packages/openid4vc-client/tests/openid4vc-client.e2e.test.ts deleted file mode 100644 index 55fe329317..0000000000 --- a/packages/openid4vc-client/tests/openid4vc-client.e2e.test.ts +++ /dev/null @@ -1,483 +0,0 @@ -import type { KeyDidCreateOptions } from '@aries-framework/core' - -import { AskarModule } from '@aries-framework/askar' -import { - JwaSignatureAlgorithm, - Agent, - KeyType, - TypedArrayEncoder, - W3cCredentialRecord, - DidKey, -} from '@aries-framework/core' -import { agentDependencies } from '@aries-framework/node' -import { ariesAskar } from '@hyperledger/aries-askar-nodejs' -import nock, { cleanAll, enableNetConnect } from 'nock' - -import { OpenId4VcClientModule } from '../src' -import { OpenIdCredentialFormatProfile } from '../src/utils/claimFormatMapping' - -import { - mattrLaunchpadJsonLd_draft_08, - // FIXME: we need a custom document loader for this, which is only present in AFJ core - // mattrLaunchpadJsonLd_draft_08, - waltIdJffJwt_draft_08, - waltIdJffJwt_draft_11, -} from './fixtures' - -const modules = { - openId4VcClient: new OpenId4VcClientModule(), - askar: new AskarModule({ - ariesAskar, - }), -} - -describe('OpenId4VcClient', () => { - let agent: Agent - - beforeEach(async () => { - agent = new Agent({ - config: { - label: 'OpenId4VcClient Test', - walletConfig: { - id: 'openid4vc-client-test', - key: 'openid4vc-client-test', - }, - }, - dependencies: agentDependencies, - modules, - }) - - await agent.initialize() - }) - - afterEach(async () => { - await agent.shutdown() - await agent.wallet.delete() - }) - - describe('[DRAFT 08]: Pre-authorized flow', () => { - afterEach(() => { - cleanAll() - enableNetConnect() - }) - - xit('[DRAFT 08]: Should successfully execute the pre-authorized flow using a did:key Ed25519 subject and JSON-LD credential', async () => { - const fixture = mattrLaunchpadJsonLd_draft_08 - /** - * Below we're setting up some mock HTTP responses. - * These responses are based on the openid-initiate-issuance URI above - * */ - - // setup temporary redirect mock - nock('https://launchpad.mattrlabs.com') - .get('/.well-known/openid-credential-issuer') - .reply(307, undefined, { - Location: 'https://launchpad.vii.electron.mattrlabs.io/.well-known/openid-credential-issuer', - }) - - .get('/.well-known/openid-configuration') - .reply(404) - - .get('/.well-known/oauth-authorization-server') - .reply(404) - - // setup server metadata response - nock('https://launchpad.vii.electron.mattrlabs.io') - .get('/.well-known/openid-credential-issuer') - .reply(200, fixture.getMetadataResponse) - - // setup access token response - .post('/oidc/v1/auth/token') - .reply(200, fixture.acquireAccessTokenResponse) - - // setup credential request response - .post('/oidc/v1/auth/credential') - .reply(200, fixture.credentialResponse) - - const did = await agent.dids.create({ - method: 'key', - options: { - keyType: KeyType.Ed25519, - }, - secret: { - privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), - }, - }) - - const didKey = DidKey.fromDid(did.didState.did as string) - const kid = `${did.didState.did as string}#${didKey.key.fingerprint}` - const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) - if (!verificationMethod) throw new Error('No verification method found') - - const w3cCredentialRecords = await agent.modules.openId4VcClient.requestCredentialUsingPreAuthorizedCode({ - issuerUri: fixture.credentialOffer, - verifyCredentialStatus: false, - // We only allow EdDSa, as we've created a did with keyType ed25519. If we create - // or determine the did dynamically we could use any signature algorithm - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - proofOfPossessionVerificationMethodResolver: () => verificationMethod, - }) - - expect(w3cCredentialRecords).toHaveLength(1) - const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord - expect(w3cCredentialRecord).toBeInstanceOf(W3cCredentialRecord) - - expect(w3cCredentialRecord.credential.type).toEqual([ - 'VerifiableCredential', - 'VerifiableCredentialExtension', - 'OpenBadgeCredential', - ]) - - expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) - }) - - it('[DRAFT 08]: Should successfully execute the pre-authorized flow using a did:key P256 subject and JWT credential', async () => { - const fixture = waltIdJffJwt_draft_08 - - nock('https://jff.walt.id/issuer-api/default/oidc') - // metadata - .get('/.well-known/openid-credential-issuer') - .reply(200, fixture.getMetadataResponse) - .get('/.well-known/openid-configuration') - .reply(404) - .get('/.well-known/oauth-authorization-server') - .reply(404) - - // setup access token response - .post('/token') - .reply(200, fixture.credentialResponse) - - // setup credential request response - .post('/credential') - .reply(200, fixture.credentialResponse) - - const did = await agent.dids.create({ - method: 'key', - options: { - keyType: KeyType.P256, - }, - secret: { - privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), - }, - }) - - const didKey = DidKey.fromDid(did.didState.did as string) - const kid = `${didKey.did}#${didKey.key.fingerprint}` - const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) - if (!verificationMethod) throw new Error('No verification method found') - - const w3cCredentialRecords = await agent.modules.openId4VcClient.requestCredentialUsingPreAuthorizedCode({ - issuerUri: fixture.credentialOffer, - allowedCredentialFormats: [OpenIdCredentialFormatProfile.JwtVcJson], - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], - proofOfPossessionVerificationMethodResolver: () => verificationMethod, - verifyCredentialStatus: false, - }) - - expect(w3cCredentialRecords[0]).toBeInstanceOf(W3cCredentialRecord) - const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord - - expect(w3cCredentialRecord.credential.type).toEqual([ - 'VerifiableCredential', - 'VerifiableAttestation', - 'VerifiableId', - ]) - - expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) - }) - }) - - describe('[DRAFT 08]: Authorization flow', () => { - afterAll(() => { - cleanAll() - enableNetConnect() - }) - - it('[DRAFT 08]: should generate a valid authorization url', async () => { - const fixture = mattrLaunchpadJsonLd_draft_08 - - // setup temporary redirect mock - nock('https://launchpad.mattrlabs.com') - .get('/.well-known/openid-credential-issuer') - .reply(307, undefined, { - Location: 'https://launchpad.vii.electron.mattrlabs.io/.well-known/openid-credential-issuer', - }) - .get('/.well-known/openid-configuration') - .reply(404) - .get('/.well-known/oauth-authorization-server') - .reply(404) - - // setup server metadata response - nock('https://launchpad.vii.electron.mattrlabs.io') - .get('/.well-known/openid-credential-issuer') - .reply(200, fixture.getMetadataResponse) - - // setup access token response - .post('/oidc/v1/auth/token') - .reply(200, fixture.acquireAccessTokenResponse) - - // setup credential request response - .post('/oidc/v1/auth/credential') - .reply(200, fixture.credentialResponse) - - const clientId = 'test-client' - - const redirectUri = 'https://example.com/cb' - const scope = ['TestCredential'] - const initiationUri = - 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=OpenBadgeCredential' - const { authorizationUrl } = await agent.modules.openId4VcClient.generateAuthorizationUrl({ - clientId, - redirectUri, - scope, - initiationUri, - }) - - const parsedUrl = new URL(authorizationUrl) - expect(authorizationUrl.startsWith('https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/authorize')).toBe( - true - ) - expect(parsedUrl.searchParams.get('response_type')).toBe('code') - expect(parsedUrl.searchParams.get('client_id')).toBe(clientId) - expect(parsedUrl.searchParams.get('code_challenge_method')).toBe('S256') - expect(parsedUrl.searchParams.get('redirect_uri')).toBe(redirectUri) - }) - - it('[DRAFT 08]: should throw if no scope is provided', async () => { - const fixture = mattrLaunchpadJsonLd_draft_08 - - // setup temporary redirect mock - nock('https://launchpad.mattrlabs.com').get('/.well-known/openid-credential-issuer').reply(307, undefined, { - Location: 'https://launchpad.vii.electron.mattrlabs.io/.well-known/openid-credential-issuer', - }) - - // setup server metadata response - nock('https://launchpad.vii.electron.mattrlabs.io') - .get('/.well-known/openid-credential-issuer') - .reply(200, fixture.getMetadataResponse) - .get('/.well-known/openid-configuration') - .reply(404) - .get('/.well-known/oauth-authorization-server') - .reply(404) - - // setup access token response - .post('/oidc/v1/auth/token') - .reply(200, fixture.acquireAccessTokenResponse) - - // setup credential request response - .post('/oidc/v1/auth/credential') - .reply(200, fixture.credentialResponse) - - // setup server metadata response - nock('https://launchpad.vii.electron.mattrlabs.io') - .get('/.well-known/openid-credential-issuer') - .reply(200, fixture.getMetadataResponse) - - const clientId = 'test-client' - const redirectUri = 'https://example.com/cb' - const initiationUri = - 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=OpenBadgeCredential' - await expect( - agent.modules.openId4VcClient.generateAuthorizationUrl({ - clientId, - redirectUri, - scope: [], - initiationUri, - }) - ).rejects.toThrow() - }) - - // Need custom document loader for this - xit('[DRAFT 08]: should successfully execute request a credential', async () => { - const fixture = mattrLaunchpadJsonLd_draft_08 - - // setup temporary redirect mock - nock('https://launchpad.mattrlabs.com').get('/.well-known/openid-credential-issuer').reply(307, undefined, { - Location: 'https://launchpad.vii.electron.mattrlabs.io/.well-known/openid-credential-issuer', - }) - - // setup server metadata response - nock('https://launchpad.vii.electron.mattrlabs.io') - .get('/.well-known/openid-credential-issuer') - .reply(200, fixture.getMetadataResponse) - .get('/.well-known/openid-configuration') - .reply(404) - .get('/.well-known/oauth-authorization-server') - .reply(404) - - // setup access token response - .post('/oidc/v1/auth/token') - .reply(200, fixture.acquireAccessTokenResponse) - - // setup credential request response - .post('/oidc/v1/auth/credential') - .reply(200, fixture.credentialResponse) - - const did = await agent.dids.create({ - method: 'key', - options: { - keyType: KeyType.Ed25519, - }, - secret: { - privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), - }, - }) - - const didKey = DidKey.fromDid(did.didState.did as string) - const kid = `${did.didState.did as string}#${didKey.key.fingerprint}` - const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) - if (!verificationMethod) throw new Error('No verification method found') - - const clientId = 'test-client' - - const redirectUri = 'https://example.com/cb' - const initiationUri = - 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=OpenBadgeCredential' - - const scope = ['TestCredential'] - const { codeVerifier } = await agent.modules.openId4VcClient.generateAuthorizationUrl({ - clientId, - redirectUri, - scope, - initiationUri, - }) - const w3cCredentialRecords = await agent.modules.openId4VcClient.requestCredentialUsingAuthorizationCode({ - clientId: clientId, - authorizationCode: 'test-code', - codeVerifier: codeVerifier, - verifyCredentialStatus: false, - proofOfPossessionVerificationMethodResolver: () => verificationMethod, - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - issuerUri: initiationUri, // TODO - redirectUri: redirectUri, - }) - - expect(w3cCredentialRecords).toHaveLength(1) - const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord - expect(w3cCredentialRecord).toBeInstanceOf(W3cCredentialRecord) - - expect(w3cCredentialRecord.credential.type).toEqual([ - 'VerifiableCredential', - 'VerifiableCredentialExtension', - 'OpenBadgeCredential', - ]) - - expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) - }) - }) - - describe('[DRAFT 11]: Pre-authorized flow', () => { - afterEach(() => { - cleanAll() - enableNetConnect() - }) - - // it('[DRAFT 11]: Should successfully execute the pre-authorized flow using a did:key Ed25519 subject and JSON-LD credential', async () => { - // const fixture = waltIdJffJwt_draft_11 - - // nock('https://jff.walt.id/issuer-api/default/oidc') - // .get('/.well-known/openid-credential-issuer') - // .reply(200, fixture.getMetadataResponse) - - // // setup access token response - // .post('/token') - // .reply(200, fixture.credentialResponse) - - // // setup credential request response - // .post('/credential') - // .reply(200, fixture.credentialResponse) - - // const did = await agent.dids.create({ - // method: 'key', - // options: { - // keyType: KeyType.Ed25519, - // }, - // secret: { - // privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), - // }, - // }) - - // const didKey = DidKey.fromDid(did.didState.did as string) - // const kid = `${did.didState.did as string}#${didKey.key.fingerprint}` - // const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) - // if (!verificationMethod) throw new Error('No verification method found') - - // const w3cCredentialRecords = await agent.modules.openId4VcClient.requestCredentialUsingPreAuthorizedCode({ - // uri: fixture.credentialOffer, - // verifyCredentialStatus: false, - // // We only allow EdDSa, as we've created a did with keyType ed25519. If we create - // // or determine the did dynamically we could use any signature algorithm - // allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - // proofOfPossessionVerificationMethodResolver: () => verificationMethod, - // }) - - // expect(w3cCredentialRecords).toHaveLength(1) - // const w3cCredentialRecord = w3cCredentialRecords[0] - // expect(w3cCredentialRecord).toBeInstanceOf(W3cCredentialRecord) - - // expect(w3cCredentialRecord.credential.type).toEqual([ - // 'VerifiableCredential', - // 'VerifiableAttestation', - // 'VerifiableId', - // ]) - - // expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) - // }) - - it('[DRAFT 11]: Should successfully execute the pre-authorized flow using a did:key P256 subject and JWT credential', async () => { - const fixture = waltIdJffJwt_draft_11 - - /** - * Below we're setting up some mock HTTP responses. - * These responses are based on the openid-initiate-issuance URI above - */ - // setup server metadata response - const httpMock = nock('https://jff.walt.id/issuer-api/default/oidc') - .get('/.well-known/openid-credential-issuer') - .reply(200, fixture.getMetadataResponse) - .get('/.well-known/openid-configuration') - .reply(404) - .get('/.well-known/oauth-authorization-server') - .reply(404) - - // setup access token response - httpMock.post('/token').reply(200, fixture.credentialResponse) - // setup credential request response - httpMock.post('/credential').reply(200, fixture.credentialResponse) - - const did = await agent.dids.create({ - method: 'key', - options: { - keyType: KeyType.P256, - }, - secret: { - privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), - }, - }) - - const didKey = DidKey.fromDid(did.didState.did as string) - const kid = `${didKey.did}#${didKey.key.fingerprint}` - const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) - if (!verificationMethod) throw new Error('No verification method found') - - const w3cCredentialRecords = await agent.modules.openId4VcClient.requestCredentialUsingPreAuthorizedCode({ - issuerUri: fixture.credentialOffer, - allowedCredentialFormats: [OpenIdCredentialFormatProfile.JwtVcJson], - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], - proofOfPossessionVerificationMethodResolver: () => verificationMethod, - verifyCredentialStatus: false, - }) - - expect(w3cCredentialRecords[0]).toBeInstanceOf(W3cCredentialRecord) - const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord - - expect(w3cCredentialRecord.credential.type).toEqual([ - 'VerifiableCredential', - 'VerifiableAttestation', - 'VerifiableId', - ]) - - expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) - }) - }) -}) diff --git a/packages/openid4vc-holder/package.json b/packages/openid4vc-holder/package.json index 3eac40e571..2f773d6aa2 100644 --- a/packages/openid4vc-holder/package.json +++ b/packages/openid4vc-holder/package.json @@ -35,7 +35,6 @@ "@aries-framework/askar": "0.4.2", "@aries-framework/node": "0.4.2", "@hyperledger/aries-askar-nodejs": "^0.1.0", - "@types/jsonpath": "^0.2.0", "nock": "^13.3.0", "rimraf": "^4.4.0", "typescript": "~4.9.5" diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts b/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts index 6468aa07e3..b45615624f 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts @@ -4,6 +4,8 @@ import { AgentConfig } from '@aries-framework/core' import { OpenId4VcHolderApi } from './OpenId4VcHolderApi' import { OpenId4VcHolderService } from './OpenId4VcHolderService' +import { PresentationExchangeService } from './presentations' +import { OpenId4VpHolderService } from './presentations/OpenId4VpHolderService' /** * @public @@ -25,7 +27,10 @@ export class OpenId4VcHolderModule implements Module { // Api dependencyManager.registerContextScoped(OpenId4VcHolderApi) + // Services // Services dependencyManager.registerSingleton(OpenId4VcHolderService) + dependencyManager.registerSingleton(OpenId4VpHolderService) + dependencyManager.registerSingleton(PresentationExchangeService) } } diff --git a/packages/openid4vc-holder/src/index.ts b/packages/openid4vc-holder/src/index.ts index c81821681c..b345b161b0 100644 --- a/packages/openid4vc-holder/src/index.ts +++ b/packages/openid4vc-holder/src/index.ts @@ -13,6 +13,7 @@ export { RequestCredentialOptions, SupportedCredentialFormats, } from './OpenId4VcHolderServiceOptions' +export * from './presentations' export { getOpenId4VcCredentialMetadata, OpenId4VcCredentialMetadata, diff --git a/packages/openid4vc-client/src/presentations/OpenId4VpClientService.ts b/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts similarity index 99% rename from packages/openid4vc-client/src/presentations/OpenId4VpClientService.ts rename to packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts index 0c8d98dbcd..ef5628bb96 100644 --- a/packages/openid4vc-client/src/presentations/OpenId4VpClientService.ts +++ b/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts @@ -43,7 +43,7 @@ function isVerifiedAuthorizationRequestWithPresentationDefinition( } @injectable() -export class OpenId4VpClientService { +export class OpenId4VpHolderService { public constructor(private presentationExchangeService: PresentationExchangeService) {} private getOp(agentContext: AgentContext) { diff --git a/packages/openid4vc-client/src/presentations/PresentationExchangeService.ts b/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts similarity index 100% rename from packages/openid4vc-client/src/presentations/PresentationExchangeService.ts rename to packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts diff --git a/packages/openid4vc-client/src/presentations/example.md b/packages/openid4vc-holder/src/presentations/example.md similarity index 100% rename from packages/openid4vc-client/src/presentations/example.md rename to packages/openid4vc-holder/src/presentations/example.md diff --git a/packages/openid4vc-client/src/presentations/fixtures.ts b/packages/openid4vc-holder/src/presentations/fixtures.ts similarity index 100% rename from packages/openid4vc-client/src/presentations/fixtures.ts rename to packages/openid4vc-holder/src/presentations/fixtures.ts diff --git a/packages/openid4vc-client/src/presentations/index.ts b/packages/openid4vc-holder/src/presentations/index.ts similarity index 78% rename from packages/openid4vc-client/src/presentations/index.ts rename to packages/openid4vc-holder/src/presentations/index.ts index 84d9845844..7624253db7 100644 --- a/packages/openid4vc-client/src/presentations/index.ts +++ b/packages/openid4vc-holder/src/presentations/index.ts @@ -1,6 +1,6 @@ export { - OpenId4VpClientService, + OpenId4VpHolderService, VerifiedAuthorizationRequestWithPresentationDefinition, -} from './OpenId4VpClientService' +} from './OpenId4VpHolderService' export { PresentationExchangeService } from './PresentationExchangeService' export { PresentationSubmission, SubmissionEntry } from './selection' diff --git a/packages/openid4vc-client/src/presentations/selection/PexCredentialSelection.ts b/packages/openid4vc-holder/src/presentations/selection/PexCredentialSelection.ts similarity index 100% rename from packages/openid4vc-client/src/presentations/selection/PexCredentialSelection.ts rename to packages/openid4vc-holder/src/presentations/selection/PexCredentialSelection.ts diff --git a/packages/openid4vc-client/src/presentations/selection/index.ts b/packages/openid4vc-holder/src/presentations/selection/index.ts similarity index 100% rename from packages/openid4vc-client/src/presentations/selection/index.ts rename to packages/openid4vc-holder/src/presentations/selection/index.ts diff --git a/packages/openid4vc-client/src/presentations/selection/types.ts b/packages/openid4vc-holder/src/presentations/selection/types.ts similarity index 100% rename from packages/openid4vc-client/src/presentations/selection/types.ts rename to packages/openid4vc-holder/src/presentations/selection/types.ts diff --git a/packages/openid4vc-client/src/presentations/transform.ts b/packages/openid4vc-holder/src/presentations/transform.ts similarity index 100% rename from packages/openid4vc-client/src/presentations/transform.ts rename to packages/openid4vc-holder/src/presentations/transform.ts diff --git a/packages/openid4vc-holder/tests/OpenId4VcClientModule.test.ts b/packages/openid4vc-holder/tests/OpenId4VcClientModule.test.ts index 46d2bb7308..f67cdbc991 100644 --- a/packages/openid4vc-holder/tests/OpenId4VcClientModule.test.ts +++ b/packages/openid4vc-holder/tests/OpenId4VcClientModule.test.ts @@ -4,6 +4,7 @@ import type { DependencyManager } from '@aries-framework/core' import { OpenId4VcHolderApi } from '../src/OpenId4VcHolderApi' import { OpenId4VcHolderModule } from '../src/OpenId4VcHolderModule' import { OpenId4VcHolderService } from '../src/OpenId4VcHolderService' +import { OpenId4VpHolderService, PresentationExchangeService } from '../src/presentations' const dependencyManager = { registerInstance: jest.fn(), @@ -20,7 +21,9 @@ describe('OpenId4VcClientModule', () => { expect(dependencyManager.registerContextScoped).toHaveBeenCalledTimes(1) expect(dependencyManager.registerContextScoped).toHaveBeenCalledWith(OpenId4VcHolderApi) - expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(3) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VcHolderService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VpHolderService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(PresentationExchangeService) }) }) diff --git a/packages/openid4vc-issuer/README.md b/packages/openid4vc-issuer/README.md new file mode 100644 index 0000000000..1a62c933de --- /dev/null +++ b/packages/openid4vc-issuer/README.md @@ -0,0 +1,68 @@ +

+
+ Hyperledger Aries logo +

+

Aries Framework JavaScript Open ID Connect For Verifiable Credentials Client Module

+

+ License + typescript + @aries-framework/openid4vc-issuer version + +

+
+ +Open ID Connect For Verifiable Credentials Issuer Module for [Aries Framework JavaScript](https://github.com/hyperledger/aries-framework-javascript). + +### Installation + +Make sure you have set up the correct version of Aries Framework JavaScript according to the AFJ repository. + +```sh +yarn add @aries-framework/openid4vc-issuer +``` + +### Quick start + +#### Requirements + +#### Module registration + +In order to get this module to work, we need to inject it into the agent. This makes the module's functionality accessible through the agent's `modules` api. + +```ts +import { OpenId4VcIssuerModule } from '@aries-framework/openid4vc-issuer' + +const agent = new Agent({ + config: { + /* config */ + }, + dependencies: agentDependencies, + modules: { + openId4VcIssuer: new OpenId4VcIssuerModule(), + /* other custom modules */ + }, +}) + +await agent.initialize() +``` + +How the module is injected and the agent has been initialized, you can access the module's functionality through `agent.modules.openId4VcIssuer`. + +#### Preparing a DID diff --git a/packages/openid4vc-client/jest.config.ts b/packages/openid4vc-issuer/jest.config.ts similarity index 100% rename from packages/openid4vc-client/jest.config.ts rename to packages/openid4vc-issuer/jest.config.ts diff --git a/packages/openid4vc-client/package.json b/packages/openid4vc-issuer/package.json similarity index 69% rename from packages/openid4vc-client/package.json rename to packages/openid4vc-issuer/package.json index 9c50ae7100..3023b6998b 100644 --- a/packages/openid4vc-client/package.json +++ b/packages/openid4vc-issuer/package.json @@ -1,5 +1,5 @@ { - "name": "@aries-framework/openid4vc-client", + "name": "@aries-framework/openid4vc-issuer", "main": "build/index", "types": "build/index", "version": "0.4.2", @@ -10,11 +10,11 @@ "publishConfig": { "access": "public" }, - "homepage": "https://github.com/hyperledger/aries-framework-javascript/tree/main/packages/openid4vc-client", + "homepage": "https://github.com/hyperledger/aries-framework-javascript/tree/main/packages/openid4vc-issuer", "repository": { "type": "git", "url": "https://github.com/hyperledger/aries-framework-javascript", - "directory": "packages/openid4vc-client" + "directory": "packages/openid4vc-issuer" }, "scripts": { "build": "yarn run clean && yarn run compile", @@ -25,22 +25,16 @@ }, "dependencies": { "@aries-framework/core": "0.4.2", - "@sphereon/did-auth-siop": "^0.4.2", - "@sphereon/oid4vci-client": "^0.7.3", + "@sphereon/oid4vci-issuer": "^0.7.3", "@sphereon/oid4vci-common": "^0.7.3", - "@sphereon/pex": "^2.1.3-unstable.6", - "@sphereon/pex-models": "^2.1.1", "@sphereon/ssi-types": "^0.17.5", "@stablelib/random": "^1.0.2", - "fast-text-encoding": "^1.0.6", - "jsonpath": "^1.1.1", - "sha.js": "^2.4.11" + "fast-text-encoding": "^1.0.6" }, "devDependencies": { "@aries-framework/askar": "0.4.2", "@aries-framework/node": "0.4.2", "@hyperledger/aries-askar-nodejs": "^0.1.0", - "@types/jsonpath": "^0.2.0", "nock": "^13.3.0", "rimraf": "^4.4.0", "typescript": "~4.9.5" diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts new file mode 100644 index 0000000000..feaff0ba8e --- /dev/null +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts @@ -0,0 +1,27 @@ +import type { IssueCredentialOptions, SendCredentialOfferOptions } from './OpenId4VcIssuerServiceOptions' + +import { injectable, AgentContext } from '@aries-framework/core' + +import { OpenId4VcIssuerService } from './OpenId4VcIssuerService' + +/** + * @public + */ +@injectable() +export class OpenId4VcIssuerApi { + private agentContext: AgentContext + private openId4VcIssuerService: OpenId4VcIssuerService + + public constructor(agentContext: AgentContext, openId4VcIssuerService: OpenId4VcIssuerService) { + this.agentContext = agentContext + this.openId4VcIssuerService = openId4VcIssuerService + } + + public sendCredentialOffer(options: SendCredentialOfferOptions) { + // TODO: Implement + } + + public issueCredential(options: IssueCredentialOptions) { + // TODO: Implement + } +} diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerModule.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerModule.ts new file mode 100644 index 0000000000..26a65a88ad --- /dev/null +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerModule.ts @@ -0,0 +1,31 @@ +import type { DependencyManager, Module } from '@aries-framework/core' + +import { AgentConfig } from '@aries-framework/core' + +import { OpenId4VcIssuerApi } from './OpenId4VcIssuerApi' +import { OpenId4VcIssuerService } from './OpenId4VcIssuerService' + +/** + * @public + */ +export class OpenId4VcIssuerModule implements Module { + public readonly api = OpenId4VcIssuerApi + + /** + * Registers the dependencies of the question answer module on the dependency manager. + */ + public register(dependencyManager: DependencyManager) { + // Warn about experimental module + dependencyManager + .resolve(AgentConfig) + .logger.warn( + "The '@aries-framework/openid4vc-issuer' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." + ) + + // Api + dependencyManager.registerContextScoped(OpenId4VcIssuerApi) + + // Services + dependencyManager.registerSingleton(OpenId4VcIssuerService) + } +} diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts new file mode 100644 index 0000000000..7a50ba6366 --- /dev/null +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts @@ -0,0 +1,32 @@ +import { + InjectionSymbols, + JwsService, + Logger, + W3cCredentialRepository, + W3cCredentialService, + inject, + injectable, +} from '@aries-framework/core' + +/** + * @internal + */ +@injectable() +export class OpenId4VcIssuerService { + private logger: Logger + private w3cCredentialService: W3cCredentialService + private w3cCredentialRepository: W3cCredentialRepository + private jwsService: JwsService + + public constructor( + @inject(InjectionSymbols.Logger) logger: Logger, + w3cCredentialService: W3cCredentialService, + w3cCredentialRepository: W3cCredentialRepository, + jwsService: JwsService + ) { + this.w3cCredentialService = w3cCredentialService + this.w3cCredentialRepository = w3cCredentialRepository + this.jwsService = jwsService + this.logger = logger + } +} diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts new file mode 100644 index 0000000000..0de5ea82f2 --- /dev/null +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts @@ -0,0 +1,7 @@ +export interface IssueCredentialOptions { + tobedefined: true +} + +export interface SendCredentialOfferOptions { + tobedefined: true +} diff --git a/packages/openid4vc-issuer/src/index.ts b/packages/openid4vc-issuer/src/index.ts new file mode 100644 index 0000000000..3be8888448 --- /dev/null +++ b/packages/openid4vc-issuer/src/index.ts @@ -0,0 +1,8 @@ +import 'fast-text-encoding' + +export * from './OpenId4VcIssuerApi' +export * from './OpenId4VcIssuerModule' +export * from './OpenId4VcIssuerService' + +// Contains internal types, so we don't export everything +export {} from './OpenId4VcIssuerServiceOptions' diff --git a/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts b/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts new file mode 100644 index 0000000000..2aa4596409 --- /dev/null +++ b/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts @@ -0,0 +1,59 @@ +import type { KeyDidCreateOptions } from '@aries-framework/core' + +import { AskarModule } from '@aries-framework/askar' +import { + JwaSignatureAlgorithm, + Agent, + KeyType, + TypedArrayEncoder, + W3cCredentialRecord, + DidKey, +} from '@aries-framework/core' +import { agentDependencies } from '@aries-framework/node' +import { ariesAskar } from '@hyperledger/aries-askar-nodejs' +import nock, { cleanAll, enableNetConnect } from 'nock' + +import { OpenId4VcIssuerModule } from '../src' + +const modules = { + openId4VcHolder: new OpenId4VcIssuerModule(), + askar: new AskarModule({ + ariesAskar, + }), +} + +describe('OpenId4VcIssuer', () => { + let agent: Agent + + beforeEach(async () => { + agent = new Agent({ + config: { + label: 'OpenId4VcIssuer Test', + walletConfig: { + id: 'openid4vc-Issuer-test', + key: 'openid4vc-Issuer-test', + }, + }, + dependencies: agentDependencies, + modules, + }) + + await agent.initialize() + }) + + afterEach(async () => { + await agent.shutdown() + await agent.wallet.delete() + }) + + describe('[DRAFT 08]: Pre-authorized flow', () => { + afterEach(() => { + cleanAll() + enableNetConnect() + }) + + it('test', async () => { + expect(true).toBe(true) + }) + }) +}) diff --git a/packages/openid4vc-client/tests/setup.ts b/packages/openid4vc-issuer/tests/setup.ts similarity index 100% rename from packages/openid4vc-client/tests/setup.ts rename to packages/openid4vc-issuer/tests/setup.ts diff --git a/packages/openid4vc-client/tsconfig.build.json b/packages/openid4vc-issuer/tsconfig.build.json similarity index 100% rename from packages/openid4vc-client/tsconfig.build.json rename to packages/openid4vc-issuer/tsconfig.build.json diff --git a/packages/openid4vc-client/tsconfig.json b/packages/openid4vc-issuer/tsconfig.json similarity index 100% rename from packages/openid4vc-client/tsconfig.json rename to packages/openid4vc-issuer/tsconfig.json diff --git a/packages/openid4vc-verifier/README.md b/packages/openid4vc-verifier/README.md new file mode 100644 index 0000000000..02adb4c47a --- /dev/null +++ b/packages/openid4vc-verifier/README.md @@ -0,0 +1,68 @@ +

+
+ Hyperledger Aries logo +

+

Aries Framework JavaScript Open ID Connect For Verifiable Credentials Client Module

+

+ License + typescript + @aries-framework/openid4vc-verifier version + +

+
+ +Open ID Connect For Verifiable Credentials Verifier Module for [Aries Framework JavaScript](https://github.com/hyperledger/aries-framework-javascript). + +### Installation + +Make sure you have set up the correct version of Aries Framework JavaScript according to the AFJ repository. + +```sh +yarn add @aries-framework/openid4vc-verifier +``` + +### Quick start + +#### Requirements + +#### Module registration + +In order to get this module to work, we need to inject it into the agent. This makes the module's functionality accessible through the agent's `modules` api. + +```ts +import { OpenId4VcVerifierModule } from '@aries-framework/openid4vc-verifier' + +const agent = new Agent({ + config: { + /* config */ + }, + dependencies: agentDependencies, + modules: { + openId4VcVerifier: new OpenId4VcVerifierModule(), + /* other custom modules */ + }, +}) + +await agent.initialize() +``` + +How the module is injected and the agent has been initialized, you can access the module's functionality through `agent.modules.openId4VcVerifier`. + +#### Preparing a DID diff --git a/packages/openid4vc-verifier/jest.config.ts b/packages/openid4vc-verifier/jest.config.ts new file mode 100644 index 0000000000..8641cf4d67 --- /dev/null +++ b/packages/openid4vc-verifier/jest.config.ts @@ -0,0 +1,14 @@ +import type { Config } from '@jest/types' + +import base from '../../jest.config.base' + +import packageJson from './package.json' + +const config: Config.InitialOptions = { + ...base, + + displayName: packageJson.name, + setupFilesAfterEnv: ['./tests/setup.ts'], +} + +export default config diff --git a/packages/openid4vc-verifier/package.json b/packages/openid4vc-verifier/package.json new file mode 100644 index 0000000000..aba1ebdfef --- /dev/null +++ b/packages/openid4vc-verifier/package.json @@ -0,0 +1,41 @@ +{ + "name": "@aries-framework/openid4vc-verifier", + "main": "build/index", + "types": "build/index", + "version": "0.4.2", + "files": [ + "build" + ], + "license": "Apache-2.0", + "publishConfig": { + "access": "public" + }, + "homepage": "https://github.com/hyperledger/aries-framework-javascript/tree/main/packages/openid4vc-verifier", + "repository": { + "type": "git", + "url": "https://github.com/hyperledger/aries-framework-javascript", + "directory": "packages/openid4vc-verifier" + }, + "scripts": { + "build": "yarn run clean && yarn run compile", + "clean": "rimraf ./build", + "compile": "tsc -p tsconfig.build.json", + "prepublishOnly": "yarn run build", + "test": "jest" + }, + "dependencies": { + "@aries-framework/core": "0.4.2", + "@sphereon/did-auth-siop": "^0.4.2", + "@sphereon/ssi-types": "^0.17.5", + "@stablelib/random": "^1.0.2", + "fast-text-encoding": "^1.0.6" + }, + "devDependencies": { + "@aries-framework/askar": "0.4.2", + "@aries-framework/node": "0.4.2", + "@hyperledger/aries-askar-nodejs": "^0.1.0", + "nock": "^13.3.0", + "rimraf": "^4.4.0", + "typescript": "~4.9.5" + } +} diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts new file mode 100644 index 0000000000..b80f1f0c75 --- /dev/null +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts @@ -0,0 +1,27 @@ +import type { IssueCredentialOptions, SendCredentialOfferOptions } from './OpenId4VcVerifierServiceOptions' + +import { injectable, AgentContext } from '@aries-framework/core' + +import { OpenId4VcVerifierService } from './OpenId4VcVerifierService' + +/** + * @public + */ +@injectable() +export class OpenId4VcVerifierApi { + private agentContext: AgentContext + private openId4VcVerifierService: OpenId4VcVerifierService + + public constructor(agentContext: AgentContext, openId4VcVerifierService: OpenId4VcVerifierService) { + this.agentContext = agentContext + this.openId4VcVerifierService = openId4VcVerifierService + } + + public sendCredentialOffer(options: SendCredentialOfferOptions) { + // TODO: Implement + } + + public issueCredential(options: IssueCredentialOptions) { + // TODO: Implement + } +} diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierModule.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierModule.ts new file mode 100644 index 0000000000..38b68df2ea --- /dev/null +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierModule.ts @@ -0,0 +1,31 @@ +import type { DependencyManager, Module } from '@aries-framework/core' + +import { AgentConfig } from '@aries-framework/core' + +import { OpenId4VcVerifierApi } from './OpenId4VcVerifierApi' +import { OpenId4VcVerifierService } from './OpenId4VcVerifierService' + +/** + * @public + */ +export class OpenId4VcVerifierModule implements Module { + public readonly api = OpenId4VcVerifierApi + + /** + * Registers the dependencies of the question answer module on the dependency manager. + */ + public register(dependencyManager: DependencyManager) { + // Warn about experimental module + dependencyManager + .resolve(AgentConfig) + .logger.warn( + "The '@aries-framework/openid4vc-verifier' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." + ) + + // Api + dependencyManager.registerContextScoped(OpenId4VcVerifierApi) + + // Services + dependencyManager.registerSingleton(OpenId4VcVerifierService) + } +} diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts new file mode 100644 index 0000000000..4c5f2891d5 --- /dev/null +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts @@ -0,0 +1,32 @@ +import { + InjectionSymbols, + JwsService, + Logger, + W3cCredentialRepository, + W3cCredentialService, + inject, + injectable, +} from '@aries-framework/core' + +/** + * @internal + */ +@injectable() +export class OpenId4VcVerifierService { + private logger: Logger + private w3cCredentialService: W3cCredentialService + private w3cCredentialRepository: W3cCredentialRepository + private jwsService: JwsService + + public constructor( + @inject(InjectionSymbols.Logger) logger: Logger, + w3cCredentialService: W3cCredentialService, + w3cCredentialRepository: W3cCredentialRepository, + jwsService: JwsService + ) { + this.w3cCredentialService = w3cCredentialService + this.w3cCredentialRepository = w3cCredentialRepository + this.jwsService = jwsService + this.logger = logger + } +} diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts new file mode 100644 index 0000000000..0de5ea82f2 --- /dev/null +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts @@ -0,0 +1,7 @@ +export interface IssueCredentialOptions { + tobedefined: true +} + +export interface SendCredentialOfferOptions { + tobedefined: true +} diff --git a/packages/openid4vc-verifier/src/index.ts b/packages/openid4vc-verifier/src/index.ts new file mode 100644 index 0000000000..e13c769e11 --- /dev/null +++ b/packages/openid4vc-verifier/src/index.ts @@ -0,0 +1,8 @@ +import 'fast-text-encoding' + +export * from './OpenId4VcVerifierApi' +export * from './OpenId4VcVerifierModule' +export * from './OpenId4VcVerifierService' + +// Contains internal types, so we don't export everything +export {} from './OpenId4VcVerifierServiceOptions' diff --git a/packages/openid4vc-verifier/tests/openid4vc-verifier.e2e.test.ts b/packages/openid4vc-verifier/tests/openid4vc-verifier.e2e.test.ts new file mode 100644 index 0000000000..fbc8310e2f --- /dev/null +++ b/packages/openid4vc-verifier/tests/openid4vc-verifier.e2e.test.ts @@ -0,0 +1,59 @@ +import type { KeyDidCreateOptions } from '@aries-framework/core' + +import { AskarModule } from '@aries-framework/askar' +import { + JwaSignatureAlgorithm, + Agent, + KeyType, + TypedArrayEncoder, + W3cCredentialRecord, + DidKey, +} from '@aries-framework/core' +import { agentDependencies } from '@aries-framework/node' +import { ariesAskar } from '@hyperledger/aries-askar-nodejs' +import nock, { cleanAll, enableNetConnect } from 'nock' + +import { OpenId4VcVerifierModule } from '../src' + +const modules = { + openId4VcHolder: new OpenId4VcVerifierModule(), + askar: new AskarModule({ + ariesAskar, + }), +} + +describe('OpenId4VcVerifier', () => { + let agent: Agent + + beforeEach(async () => { + agent = new Agent({ + config: { + label: 'OpenId4VcVerifier Test', + walletConfig: { + id: 'openid4vc-Verifier-test', + key: 'openid4vc-Verifier-test', + }, + }, + dependencies: agentDependencies, + modules, + }) + + await agent.initialize() + }) + + afterEach(async () => { + await agent.shutdown() + await agent.wallet.delete() + }) + + describe('[DRAFT 08]: Pre-authorized flow', () => { + afterEach(() => { + cleanAll() + enableNetConnect() + }) + + it('test', async () => { + expect(true).toBe(true) + }) + }) +}) diff --git a/packages/openid4vc-verifier/tests/setup.ts b/packages/openid4vc-verifier/tests/setup.ts new file mode 100644 index 0000000000..34e38c9705 --- /dev/null +++ b/packages/openid4vc-verifier/tests/setup.ts @@ -0,0 +1 @@ +jest.setTimeout(120000) diff --git a/packages/openid4vc-verifier/tsconfig.build.json b/packages/openid4vc-verifier/tsconfig.build.json new file mode 100644 index 0000000000..2b075bbd85 --- /dev/null +++ b/packages/openid4vc-verifier/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "./build", + "skipLibCheck": true + }, + "include": ["src/**/*"] +} diff --git a/packages/openid4vc-verifier/tsconfig.json b/packages/openid4vc-verifier/tsconfig.json new file mode 100644 index 0000000000..c1aca0e050 --- /dev/null +++ b/packages/openid4vc-verifier/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["jest"], + "skipLibCheck": true + } +} From 93200ecc4d3d665d90237d6770707e119f530424 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 10 Oct 2023 15:23:29 +0200 Subject: [PATCH 006/115] fix: package.json Signed-off-by: Martin Auer --- packages/openid4vc-holder/package.json | 5 +++++ yarn.lock | 13 +++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/openid4vc-holder/package.json b/packages/openid4vc-holder/package.json index 2f773d6aa2..7a340e6d44 100644 --- a/packages/openid4vc-holder/package.json +++ b/packages/openid4vc-holder/package.json @@ -27,6 +27,10 @@ "@aries-framework/core": "0.4.2", "@sphereon/oid4vci-client": "^0.7.3", "@sphereon/oid4vci-common": "^0.7.3", + "@sphereon/did-auth-siop": "^0.4.2", + "@sphereon/pex": "^2.1.3-unstable.6", + "@sphereon/pex-models": "^2.1.1", + "jsonpath": "1.1.1", "@sphereon/ssi-types": "^0.17.5", "@stablelib/random": "^1.0.2", "fast-text-encoding": "^1.0.6" @@ -35,6 +39,7 @@ "@aries-framework/askar": "0.4.2", "@aries-framework/node": "0.4.2", "@hyperledger/aries-askar-nodejs": "^0.1.0", + "@types/jsonpath": "^0.2.1", "nock": "^13.3.0", "rimraf": "^4.4.0", "typescript": "~4.9.5" diff --git a/yarn.lock b/yarn.lock index e34e4e53ce..8a39dd29d6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2457,6 +2457,15 @@ cross-fetch "^3.1.8" jwt-decode "^3.1.2" +"@sphereon/oid4vci-issuer@^0.7.3": + version "0.7.3" + resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-issuer/-/oid4vci-issuer-0.7.3.tgz#862c34b9e4c3c3c95485971cd58471729e4b20db" + integrity sha512-1bS/Z5M5HIU84j7DbUzJ7L+yDGYMG7i8Fm/QEL7QyWfhm9puZcInQwdzcW9VVx6qoNXmFRhBPwcJcNX9C89A/w== + dependencies: + "@sphereon/oid4vci-common" "0.7.3" + "@sphereon/ssi-types" "0.17.2" + uuid "^9.0.0" + "@sphereon/pex-models@^2.0.3", "@sphereon/pex-models@^2.1.0", "@sphereon/pex-models@^2.1.1": version "2.1.1" resolved "https://registry.yarnpkg.com/@sphereon/pex-models/-/pex-models-2.1.1.tgz#399e529db2a7e3b9abbd7314cdba619ceb6cb758" @@ -2886,7 +2895,7 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/jsonpath@^0.2.0": +"@types/jsonpath@^0.2.1": version "0.2.1" resolved "https://registry.yarnpkg.com/@types/jsonpath/-/jsonpath-0.2.1.tgz#92a5f0328a58848449dd52249cbba270364e82e5" integrity sha512-CmRqkJfGIthwvW6vbNeY8wI3opKqnvX8+ec83PcK14Ee3RSla1ErAFeY/gVsh42Dm/uLCnD+pkQEDDkKuBK2bQ== @@ -7963,7 +7972,7 @@ jsonparse@^1.2.0, jsonparse@^1.3.1: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== -jsonpath@^1.1.1: +jsonpath@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/jsonpath/-/jsonpath-1.1.1.tgz#0ca1ed8fb65bb3309248cc9d5466d12d5b0b9901" integrity sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w== From 10e9bfa5b0bda0ba4d8df5c27dc052d060c7b1bb Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 11 Oct 2023 11:41:30 +0200 Subject: [PATCH 007/115] fix: rename client to holder Signed-off-by: Martin Auer --- ...penId4VcClientModule.test.ts => OpenId4VcHolderModule.test.ts} | 0 ...{openid4vc-client.e2e.test.ts => openid4vc-holder.e2e.test.ts} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename packages/openid4vc-holder/tests/{OpenId4VcClientModule.test.ts => OpenId4VcHolderModule.test.ts} (100%) rename packages/openid4vc-holder/tests/{openid4vc-client.e2e.test.ts => openid4vc-holder.e2e.test.ts} (100%) diff --git a/packages/openid4vc-holder/tests/OpenId4VcClientModule.test.ts b/packages/openid4vc-holder/tests/OpenId4VcHolderModule.test.ts similarity index 100% rename from packages/openid4vc-holder/tests/OpenId4VcClientModule.test.ts rename to packages/openid4vc-holder/tests/OpenId4VcHolderModule.test.ts diff --git a/packages/openid4vc-holder/tests/openid4vc-client.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts similarity index 100% rename from packages/openid4vc-holder/tests/openid4vc-client.e2e.test.ts rename to packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts From 41c681e3a9c1667df4288d7ecdcd5caa2818c42c Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 16 Oct 2023 17:18:38 +0200 Subject: [PATCH 008/115] feat: change the shape of the holder api Signed-off-by: Martin Auer --- .../src/OpenId4VcHolderApi.ts | 26 +- .../src/OpenId4VcHolderService.ts | 397 ++++++++++-------- .../src/OpenId4VcHolderServiceOptions.ts | 58 ++- .../src/utils/IssuerMetadataUtils.ts | 27 +- .../tests/openid4vc-holder.e2e.test.ts | 99 +++-- 5 files changed, 346 insertions(+), 261 deletions(-) diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts index c2c14d6ba6..8bd238c031 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts @@ -2,8 +2,10 @@ import type { GenerateAuthorizationUrlOptions, PreAuthCodeFlowOptions, AuthCodeFlowOptions, + ResolvedCredentialOffer, } from './OpenId4VcHolderServiceOptions' import type { W3cCredentialRecord } from '@aries-framework/core' +import type { CredentialOfferPayloadV1_0_11 } from '@sphereon/oid4vci-common' import { injectable, AgentContext } from '@aries-framework/core' @@ -23,24 +25,40 @@ export class OpenId4VcHolderApi { this.openId4VcHolderService = openId4VcHolderService } - public async requestCredentialUsingPreAuthorizedCode( + public async resolveLegacyCredentialOffer(issuerUri: string) { + const resolved = await this.openId4VcHolderService.resolveLegacyCredentialOffer(issuerUri) + return resolved + } + + public async resolveCredentialOffer(credentialOffer: string | CredentialOfferPayloadV1_0_11) { + const resolved = await this.openId4VcHolderService.resolveCredentialOffer(credentialOffer) + return resolved + } + + public async acceptCredentialOfferUsingPreAuthorizedCode( + resolvedCredentialOffer: ResolvedCredentialOffer, options: PreAuthCodeFlowOptions ): Promise { // set defaults const verifyRevocationState = options.verifyCredentialStatus ?? true - return this.openId4VcHolderService.requestCredential(this.agentContext, { + return this.openId4VcHolderService.acceptCredentialOffer(this.agentContext, { + ...resolvedCredentialOffer, ...options, verifyCredentialStatus: verifyRevocationState, flowType: AuthFlowType.PreAuthorizedCodeFlow, }) } - public async requestCredentialUsingAuthorizationCode(options: AuthCodeFlowOptions): Promise { + public async acceptCredentialOfferUsingAuthorizationCode( + resolvedCredentialOffer: ResolvedCredentialOffer, + options: AuthCodeFlowOptions + ): Promise { // set defaults const checkRevocationState = options.verifyCredentialStatus ?? true - return this.openId4VcHolderService.requestCredential(this.agentContext, { + return this.openId4VcHolderService.acceptCredentialOffer(this.agentContext, { + ...resolvedCredentialOffer, ...options, verifyCredentialStatus: checkRevocationState, flowType: AuthFlowType.AuthorizationCodeFlow, diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderService.ts b/packages/openid4vc-holder/src/OpenId4VcHolderService.ts index 31bc7c1ee1..08e3af3e67 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderService.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderService.ts @@ -1,60 +1,74 @@ import type { GenerateAuthorizationUrlOptions, - RequestCredentialOptions, + ProofOfPossessionRequirements, ProofOfPossessionVerificationMethodResolver, + RequestCredentialOptions, SupportedCredentialFormats, - ProofOfPossessionRequirements, } from './OpenId4VcHolderServiceOptions' import type { OpenIdCredentialFormatProfile } from './utils' import type { AgentContext, - W3cVerifiableCredential, - VerificationMethod, JwaSignatureAlgorithm, + VerificationMethod, + W3cVerifiableCredential, W3cVerifyCredentialResult, } from '@aries-framework/core' import type { CredentialOfferFormat, - CredentialOfferPayloadV1_0_08, - CredentialOfferRequestWithBaseUrl, + CredentialOfferPayloadV1_0_11, CredentialResponse, CredentialSupported, + EndpointMetadataResult, Jwt, OpenIDResponse, ProofOfPossessionCallbacks, + UniformCredentialOffer, + UniformCredentialOfferPayload, } from '@sphereon/oid4vci-common' import { - W3cCredentialRecord, - ClaimFormat, - getJwkClassFromJwaSignatureAlgorithm, - W3cJwtVerifiableCredential, AriesFrameworkError, - getKeyFromVerificationMethod, + ClaimFormat, Hasher, - inject, - injectable, InjectionSymbols, JsonEncoder, JsonTransformer, + JwsService, + Logger, + SignatureSuiteRegistry, TypedArrayEncoder, + W3cCredentialRecord, + W3cCredentialRepository, + W3cCredentialService, W3cJsonLdVerifiableCredential, + W3cJwtVerifiableCredential, + getJwkClassFromJwaSignatureAlgorithm, + getJwkClassFromKeyType, getJwkFromKey, + getKeyFromVerificationMethod, getSupportedVerificationMethodTypesFromKeyType, - getJwkClassFromKeyType, + inject, + injectable, parseDid, - SignatureSuiteRegistry, - JwsService, - Logger, - W3cCredentialService, - W3cCredentialRepository, } from '@aries-framework/core' -import { CredentialRequestClientBuilder, OpenID4VCIClient, ProofOfPossessionBuilder } from '@sphereon/oid4vci-client' -import { AuthzFlowType, CodeChallengeMethod, OpenId4VCIVersion } from '@sphereon/oid4vci-common' +import { + AccessTokenClient, + CredentialOfferClient, + CredentialRequestClientBuilder, + MetadataClient, + OpenID4VCIClient, + ProofOfPossessionBuilder, +} from '@sphereon/oid4vci-client' +import { + OpenId4VCIVersion, + AuthzFlowType, + CodeChallengeMethod, + assertedUniformCredentialOffer, +} from '@sphereon/oid4vci-common' import { randomStringForEntropy } from '@stablelib/random' -import { supportedCredentialFormats, AuthFlowType } from './OpenId4VcHolderServiceOptions' -import { setOpenId4VcCredentialMetadata, fromOpenIdCredentialFormatProfileToDifClaimFormat } from './utils' +import { AuthFlowType, supportedCredentialFormats } from './OpenId4VcHolderServiceOptions' +import { fromOpenIdCredentialFormatProfileToDifClaimFormat, setOpenId4VcCredentialMetadata } from './utils' import { getUniformFormat } from './utils/Formats' import { getSupportedCredentials } from './utils/IssuerMetadataUtils' @@ -112,6 +126,7 @@ export class OpenId4VcHolderService { ) } + // TODO: how should people get this URI const client = await OpenID4VCIClient.fromURI({ uri: options.initiationUri, flowType: AuthzFlowType.AUTHORIZATION_CODE_FLOW, @@ -141,18 +156,74 @@ export class OpenId4VcHolderService { } } - public async requestCredential(agentContext: AgentContext, options: RequestCredentialOptions) { - const receivedCredentials: W3cCredentialRecord[] = [] - const supportedJwaSignatureAlgorithms = this.getSupportedJwaSignatureAlgorithms(agentContext) + public async resolveLegacyCredentialOffer(uri: string) { + const credentialOfferWithBaseUrl = await CredentialOfferClient.fromURI(uri) + const credentialOffer = credentialOfferWithBaseUrl.credential_offer - const allowedProofOfPossessionSignatureAlgorithms = options.allowedProofOfPossessionSignatureAlgorithms - ? options.allowedProofOfPossessionSignatureAlgorithms.filter((algorithm) => - supportedJwaSignatureAlgorithms.includes(algorithm) - ) - : supportedJwaSignatureAlgorithms + return this.resolveCredentialOffer(credentialOffer, { version: credentialOfferWithBaseUrl.version }) + } + + private getFormatAndTypesFromOfferedCredential( + offeredCredential: OfferedCredentialsWithMetadata, + version: OpenId4VCIVersion + ) { + if (offeredCredential.type === OfferedCredentialType.InlineCredentialOffer) { + const { format, types } = offeredCredential.inlineCredentialOffer + return { format: format as SupportedCredentialFormats, types } + } else { + const { format, types } = offeredCredential.credentialSupported + const uniFormat = + version < OpenId4VCIVersion.VER_1_0_11 ? (getUniformFormat(format) as SupportedCredentialFormats) : format + return { format: uniFormat, types } + } + } + + public async resolveCredentialOffer( + credentialOffer: UniformCredentialOfferPayload | string, + opts?: { version?: OpenId4VCIVersion } + ) { + const version = opts?.version ?? OpenId4VCIVersion.VER_1_0_11 + const uniformCredentialOffer: UniformCredentialOffer = { + credential_offer: typeof credentialOffer === 'string' ? undefined : credentialOffer, + credential_offer_uri: typeof credentialOffer === 'string' ? credentialOffer : undefined, + } + const assertedCredentialOffer = (await assertedUniformCredentialOffer(uniformCredentialOffer)).credential_offer + const issuer = assertedCredentialOffer.credential_issuer + + const metadata = await MetadataClient.retrieveAllMetadata(issuer) + + this.logger.info('Fetched server metadata', { + issuer: metadata.issuer, + credentialEndpoint: metadata.credential_endpoint, + tokenEndpoint: metadata.token_endpoint, + }) - // Take the allowed credential formats from the options or use the default - const allowedCredentialFormats = options.allowedCredentialFormats ?? supportedCredentialFormats + this.logger.debug('Full server metadata', metadata) + + if (!metadata) { + throw new AriesFrameworkError(`Could not retrieve metadata for OpenID4VCI issuer: ${issuer}`) + } + + const offeredCredentialsWithMetadata = this.getOfferedCredentialsWithMetadata( + assertedCredentialOffer, + metadata.credentialIssuerMetadata, + version + ) + + const credentialsToRequest = offeredCredentialsWithMetadata.map((offeredCredential) => + this.getFormatAndTypesFromOfferedCredential(offeredCredential, version) + ) + + return { + metadata, + credentialOfferPayload: assertedCredentialOffer, + credentialsToRequest, + version, + } + } + + public async acceptCredentialOffer(agentContext: AgentContext, options: RequestCredentialOptions) { + const { credentialsToRequest, credentialOfferPayload, metadata, version } = options const flowType = flowTypeMapping[options.flowType] if (!flowType) { @@ -161,62 +232,79 @@ export class OpenId4VcHolderService { ) } - const client = await OpenID4VCIClient.fromURI({ - uri: options.issuerUri, - flowType, - retrieveServerMetadata: false, - }) + const supportedJwaSignatureAlgorithms = this.getSupportedJwaSignatureAlgorithms(agentContext) + + const allowedProofOfPossessionSignatureAlgorithms = options.allowedProofOfPossessionSignatureAlgorithms + ? options.allowedProofOfPossessionSignatureAlgorithms.filter((algorithm) => + supportedJwaSignatureAlgorithms.includes(algorithm) + ) + : supportedJwaSignatureAlgorithms - const serverMetadata = await client.retrieveServerMetadata() + if (allowedProofOfPossessionSignatureAlgorithms.length === 0) { + throw new AriesFrameworkError(`No supported proof of possession signature algorithms found.`) + } - this.logger.info('Fetched server metadata', { - issuer: serverMetadata.issuer, - credentialEndpoint: serverMetadata.credential_endpoint, - tokenEndpoint: serverMetadata.token_endpoint, - }) + const receivedCredentials: W3cCredentialRecord[] = [] - this.logger.debug('Full server metadata', serverMetadata) + const allowedCredentialFormats = supportedCredentialFormats + // TODO: how to request specific credentials with the pre-auth flow? // acquire the access token // NOTE: only scope based flow is supported for authorized flow. However there's not clear mapping between // the scope property and which credential to request (this is out of scope of the spec), so it will still // just request all credentials that have been offered in the credential offer. We may need to add some extra // input properties that allows to define the credential type(s) to request. - const accessToken = + const accessTokenClient = new AccessTokenClient() + const openIdAccessTokenResponse = options.flowType === AuthFlowType.AuthorizationCodeFlow - ? await client.acquireAccessToken({ - clientId: options.clientId, + ? await accessTokenClient.acquireAccessToken({ + metadata, + credentialOffer: { + credential_offer: credentialOfferPayload, + }, code: options.authorizationCode, codeVerifier: options.codeVerifier, redirectUri: options.redirectUri, }) - : await client.acquireAccessToken({}) // TODO: PIN + : await accessTokenClient.acquireAccessToken({ + metadata, + credentialOffer: { + credential_offer: credentialOfferPayload, + }, + pin: options.userPin, + }) - // Loop through all the credentialTypes in the credential offer - for (const offeredCredential of this.getOfferedCredentialsWithMetadata(client)) { - const format = ( - isInlineCredentialOffer(offeredCredential) - ? offeredCredential.inlineCredentialOffer.format - : offeredCredential.credentialSupported.format - ) as SupportedCredentialFormats // TODO: can we remove the cast? - - // TODO: support inline credential offers. Not clear to me how to determine the did method / alg, etc.. - if (offeredCredential.type === OfferedCredentialType.InlineCredentialOffer) { - // Check if the format is supported/allowed - if (!allowedCredentialFormats.includes(format)) continue - } else { - const supportedCredentialMetadata = offeredCredential.credentialSupported + if (!openIdAccessTokenResponse.successBody) { + throw new AriesFrameworkError(`could not acquire access token from '${metadata.issuer}'`) + } + const accessToken = openIdAccessTokenResponse.successBody - // FIXME - // TODO: that is not a must v11 could end in the same way - // If the credential id ends with the format, it is a v8 credential supported that has been - // split into multiple entries (each entry can now only have one format). For now we continue - // as assume there will be another entry with the correct format. - if (supportedCredentialMetadata.id?.endsWith(`-${supportedCredentialMetadata.format}`)) { - const uniformFormat = getUniformFormat(supportedCredentialMetadata.format) as SupportedCredentialFormats - if (!allowedCredentialFormats.includes(uniformFormat)) continue - } - } + const issuerMetadata = metadata.credentialIssuerMetadata + if (!issuerMetadata) throw new AriesFrameworkError('Found no credential issuer metadata') + + const offeredCredentialsWithMetadata = this.getOfferedCredentialsWithMetadata( + credentialOfferPayload, + issuerMetadata, + version + ) + + const credentialsToRequestWithMetadata = credentialsToRequest?.map((ctr) => { + const credentialToRequest = offeredCredentialsWithMetadata.find((offeredCredentialWithMetadata) => { + const { format, types } = this.getFormatAndTypesFromOfferedCredential(offeredCredentialWithMetadata, version) + return ctr.format === format && ctr.types.sort().join(',') === types.sort().join(',') + }) + + if (!credentialToRequest) + throw new AriesFrameworkError( + `Could not find the the requested credential with format '${ctr.format}' and types '${ctr.types}' in the offered credentials` + ) + + return credentialToRequest + }) + + // Loop through all the credentialTypes in the credential offer + for (const offeredCredential of credentialsToRequestWithMetadata ?? offeredCredentialsWithMetadata) { + const isInlineOffer = isInlineCredentialOffer(offeredCredential) // Get all options for the credential request (such as which kid to use, the signature algorithm, etc) const { verificationMethod, signatureAlgorithm } = await this.getCredentialRequestOptions(agentContext, { @@ -228,16 +316,15 @@ export class OpenId4VcHolderService { const callbacks: ProofOfPossessionCallbacks = { signCallback: this.signCallback(agentContext, verificationMethod), - // TODO: verify callback } // Create the proof of possession const proofInput = await ProofOfPossessionBuilder.fromAccessTokenResponse({ accessTokenResponse: accessToken, callbacks, - version: client.version(), + version, }) - .withEndpointMetadata(serverMetadata) + .withEndpointMetadata(metadata) .withAlg(signatureAlgorithm) .withClientId(verificationMethod.controller) .withKid(verificationMethod.id) @@ -246,28 +333,36 @@ export class OpenId4VcHolderService { this.logger.debug('Generated JWS', proofInput) // Acquire the credential - const credentialRequestClient = // TODO: don't use the uri not actual anymore https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0-08.html - ( - await CredentialRequestClientBuilder.fromURI({ - uri: options.issuerUri, - metadata: serverMetadata, - }) - ) - .withTokenFromResponse(accessToken) - .build() + const credentialRequestBuilder = new CredentialRequestClientBuilder() + credentialRequestBuilder + .withVersion(version) + .withCredentialEndpoint(metadata.credential_endpoint) + .withTokenFromResponse(accessToken) + + let credentialTypes: string | string[] + if (version < OpenId4VCIVersion.VER_1_0_11) { + if (isInlineOffer) throw new AriesFrameworkError(`Inline credential offers not supported for version < 11`) + // TODO: this is wrong, how can we determine the credential types for v8? If more then 1 type is provided? + credentialTypes = offeredCredential.credentialSupported.types + } else { + if (isInlineOffer) credentialTypes = offeredCredential.inlineCredentialOffer.types + else credentialTypes = offeredCredential.credentialSupported.types + } + + const credentialRequestClient = credentialRequestBuilder.build() let credentialResponse: OpenIDResponse if (isInlineCredentialOffer(offeredCredential)) { credentialResponse = await credentialRequestClient.acquireCredentialsUsingProof({ proofInput, - credentialTypes: offeredCredential.inlineCredentialOffer.types, + credentialTypes, format: offeredCredential.inlineCredentialOffer.format, }) } else { credentialResponse = await credentialRequestClient.acquireCredentialsUsingProof({ proofInput, - credentialTypes: offeredCredential.type, + credentialTypes, format: offeredCredential.credentialSupported.format, }) } @@ -286,14 +381,9 @@ export class OpenId4VcHolderService { this.logger.debug('Full credential', credentialRecord) if (!isInlineCredentialOffer(offeredCredential)) { - const issuerMetadata = client.endpointMetadata.credentialIssuerMetadata - if (!issuerMetadata) { - // TODO: this should not happen - throw new AriesFrameworkError('Issuer metadata not found') - } const supportedCredentialMetadata = offeredCredential.credentialSupported // Set the OpenId4Vc credential metadata and update record - setOpenId4VcCredentialMetadata(credentialRecord, supportedCredentialMetadata, serverMetadata, issuerMetadata) + setOpenId4VcCredentialMetadata(credentialRecord, supportedCredentialMetadata, metadata, issuerMetadata) } receivedCredentials.push(credentialRecord) @@ -336,13 +426,15 @@ export class OpenId4VcHolderService { const supportedVerificationMethods = getSupportedVerificationMethodTypesFromKeyType(JwkClass.keyType) - const format = isInlineCredentialOffer(options.offeredCredentialWithMetadata) - ? options.offeredCredentialWithMetadata.inlineCredentialOffer.format - : options.offeredCredentialWithMetadata.credentialSupported.format + const format = ( + isInlineCredentialOffer(options.offeredCredentialWithMetadata) + ? options.offeredCredentialWithMetadata.inlineCredentialOffer.format + : options.offeredCredentialWithMetadata.credentialSupported.format + ) as SupportedCredentialFormats // Now we need to determine the did method and alg based on the cryptographic suite const verificationMethod = await options.proofOfPossessionVerificationMethodResolver({ - credentialFormat: format as SupportedCredentialFormats, + credentialFormat: format, proofOfPossessionSignatureAlgorithm: signatureAlgorithm, supportedVerificationMethods, keyType: JwkClass.keyType, @@ -379,68 +471,6 @@ export class OpenId4VcHolderService { return { verificationMethod, signatureAlgorithm } } - // TODO: i cannot view this - // todo https://sphereon.atlassian.net/browse/VDX-184 - /** - * Returns all entries from the credential offer. This includes both 'id' entries that reference a supported credential in the issuer metadata, - * as well as inline credential offers that do not reference a supported credential in the issuer metadata. - */ - private getOfferedCredentials( - credentialOfferRequestWithBaseUrl: CredentialOfferRequestWithBaseUrl - ): Array { - if (credentialOfferRequestWithBaseUrl.version < OpenId4VCIVersion.VER_1_0_11) { - const credentialOffer = - credentialOfferRequestWithBaseUrl.original_credential_offer as CredentialOfferPayloadV1_0_08 - - return typeof credentialOffer.credential_type === 'string' - ? [credentialOffer.credential_type] - : credentialOffer.credential_type - } else { - return credentialOfferRequestWithBaseUrl.credential_offer.credentials - } - } - - /** - * Return a normalized version of the credentials supported by the issuer. Can optionally filter based on the credentials - * that were offered, or the type of credentials that are supported. - * - * - * NOTE: for v1_0-08, a single credential id in the issuer metadata could have multiple formats. When retrieving the - * supported credentials, for v1_0-08, the format is appended to the id if there are multiple formats supported for - * that credential id. E.g. if the issuer metadata for v1_0-08 contains an entry with key `OpenBadgeCredential` and - * the supported formats are `jwt_vc-jsonld` and `ldp_vc`, then the id in the credentials supported will be - * `OpenBadgeCredential-jwt_vc-jsonld` and `OpenBadgeCredential-ldp_vc`, even though the offered credential is simply - * `OpenBadgeCredential`. - * - * NOTE: this method only returns the credentials supported by the issuer metadata. It does not take into account the inline - * credentials offered. Use {@link getOfferedCredentialsWithMetadata} to get both the inline and referenced offered credentials. - */ - private getCredentialsSupported( - client: OpenID4VCIClient, - restrictToOfferIds: boolean, - credentialSupportedId?: string - ): CredentialSupported[] { - const offeredIds = this.getOfferedCredentials(client.credentialOffer).filter( - (c): c is string => typeof c === 'string' - ) - - const credentialSupportedIds = restrictToOfferIds ? offeredIds : undefined - - const credentialsSupported = getSupportedCredentials({ - issuerMetadata: client.endpointMetadata.credentialIssuerMetadata, - version: client.version(), - credentialSupportedIds, - }) - - return credentialSupportedId - ? credentialsSupported.filter( - (credentialSupported) => - credentialSupported.id === credentialSupportedId || - credentialSupported.id === `${credentialSupportedId}-${credentialSupported.format}` - ) - : credentialsSupported - } - /** * Returns all entries from the credential offer with the associated metadata resolved. For inline entries, the offered credential object * is included directly. For 'id' entries, the associated `credentials_supported` object is resolved from the issuer metadata. @@ -449,45 +479,40 @@ export class OpenId4VcHolderService { * from this method could contain multiple entries for a single credential id, but with different formats. This is detectable as the * id will be the `-`. */ - private getOfferedCredentialsWithMetadata = (client: OpenID4VCIClient) => { + private getOfferedCredentialsWithMetadata = ( + credentialOfferPayload: CredentialOfferPayloadV1_0_11, + issuerMetadata: EndpointMetadataResult['credentialIssuerMetadata'], + version: OpenId4VCIVersion + ) => { const offeredCredentials: Array = [] - for (const offeredCredential of this.getOfferedCredentials(client.credentialOffer)) { - // If the offeredCredential is a string, it references a supported credential in the issuer metadata + const supportedCredentials = getSupportedCredentials({ issuerMetadata, version }) + + for (const offeredCredential of credentialOfferPayload.credentials) { + // If the offeredCredential is a string, it has to reference a supported credential in the issuer metadata if (typeof offeredCredential === 'string') { - const credentialsSupported = this.getCredentialsSupported(client, false, offeredCredential) + const foundSupportedCredentials = supportedCredentials.filter( + (supportedCredential) => + supportedCredential.id === offeredCredential || + supportedCredential.id === `${offeredCredential}-${supportedCredential.format}` + ) // Make sure the issuer metadata includes the offered credential. - if (credentialsSupported.length === 0) { + if (foundSupportedCredentials.length === 0) { throw new Error( - `Offered credential '${offeredCredential}' is not present in the credentials_supported of the issuer metadata` + `Offered credential '${offeredCredential}' is not part of credentials_supported of the issuer metadata` ) } - offeredCredentials.push( - ...credentialsSupported.map((credentialSupported) => { - return { credentialSupported, type: OfferedCredentialType.CredentialSupported } as const - }) - ) + for (const foundSupportedCredential of foundSupportedCredentials) { + offeredCredentials.push({ + credentialSupported: foundSupportedCredential, + type: OfferedCredentialType.CredentialSupported, + } as const) + } } // Otherwise it's an inline credential offer that does not reference a supported credential in the issuer metadata else { - // TODO: we could transform the inline offer to the `CredentialSupported` format, but we'll only be able to populate - // the `format`, `types` and `@context` fields. It's not really clear how to determine the supported did methods, - // signature suites, etc.. for these inline credentials. - // We should also add a property to indicate to the user that this is an inline credential offer. - // if (offeredCredential.format === 'jwt_vc_json') { - // const supported = { - // format: offeredCredential.format, - // types: offeredCredential.types, - // } satisfies CredentialSupportedJwtVcJson; - // } else if (offeredCredential.format === 'jwt_vc_json-ld' || offeredCredential.format === 'ldp_vc') { - // const supported = { - // format: offeredCredential.format, - // '@context': offeredCredential.credential_definition['@context'], - // types: offeredCredential.credential_definition.types, - // } satisfies CredentialSupported; - // } offeredCredentials.push({ inlineCredentialOffer: offeredCredential, type: OfferedCredentialType.InlineCredentialOffer, diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts index 5c62a2145b..38fcb44fbc 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts @@ -1,4 +1,5 @@ import type { JwaSignatureAlgorithm, KeyType, VerificationMethod } from '@aries-framework/core' +import type { CredentialOfferPayloadV1_0_11, EndpointMetadataResult, OpenId4VCIVersion } from '@sphereon/oid4vci-common' import { OpenIdCredentialFormatProfile } from './utils/claimFormatMapping' @@ -12,23 +13,31 @@ export const supportedCredentialFormats = [ OpenIdCredentialFormatProfile.LdpVc, ] satisfies OpenIdCredentialFormatProfile[] +export interface CredentialToRequest { + format: string + types: string[] +} + +export interface ResolvedCredentialOffer { + metadata: EndpointMetadataResult + credentialOfferPayload: CredentialOfferPayloadV1_0_11 + version: OpenId4VCIVersion + credentialsToRequest: CredentialToRequest[] +} + /** * Options that are used for the pre-authorized code flow. */ export interface PreAuthCodeFlowOptions { - issuerUri: string - verifyCredentialStatus: boolean - /** - * A list of allowed credential formats in order of preference. - * - * If the issuer supports one of the allowed formats, that first format that is supported - * from the list will be used. - * - * If the issuer doesn't support any of the allowed formats, an error is thrown - * and the request is aborted. + * String value containing a user PIN. This value MUST be present if user_pin_required was set to true in the Credential Offer. + * This parameter MUST only be used, if the grant_type is urn:ietf:params:oauth:grant-type:pre-authorized_code. */ - allowedCredentialFormats?: SupportedCredentialFormats[] + userPin?: string + + credentialsToRequest?: CredentialToRequest[] + + verifyCredentialStatus: boolean /** * A list of allowed proof of possession signature algorithms in order of preference. @@ -174,12 +183,33 @@ export enum AuthFlowType { PreAuthorizedCodeFlow, } -type WithFlowType = Options & { flowType: FlowType } +type WithInternalOptions = Options & { + flowType: FlowType + + /** + * The endpoint metadata received from the credential issuer. + * This is obtained manually or by calling the `resolveCredentialOffer` method. + */ + // TODO: reduce this to contain only the issuer metdata // + metadata: EndpointMetadataResult + + /** + * The resolved credential offer payload that was received from the issuer. + * This is obtained manually or by calling the `resolveCredentialOffer` method. + */ + credentialOfferPayload: CredentialOfferPayloadV1_0_11 + + /** + * The openid4vci specification version. + * This is obtained manually or by calling the `resolveCredentialOffer` method. + */ + version: OpenId4VCIVersion +} /** * The options that are used to request a credential from an issuer. * @internal */ export type RequestCredentialOptions = - | WithFlowType - | WithFlowType + | WithInternalOptions + | WithInternalOptions diff --git a/packages/openid4vc-holder/src/utils/IssuerMetadataUtils.ts b/packages/openid4vc-holder/src/utils/IssuerMetadataUtils.ts index 827cfdaa5e..3fda40e8d1 100644 --- a/packages/openid4vc-holder/src/utils/IssuerMetadataUtils.ts +++ b/packages/openid4vc-holder/src/utils/IssuerMetadataUtils.ts @@ -12,14 +12,13 @@ import { OpenId4VCIVersion } from '@sphereon/oid4vci-common' export function getSupportedCredentials(opts?: { issuerMetadata?: CredentialIssuerMetadata | IssuerMetadataV1_0_08 version: OpenId4VCIVersion - credentialSupportedIds?: string[] }): CredentialSupported[] { const { issuerMetadata } = opts ?? {} let credentialsSupported: CredentialSupported[] if (!issuerMetadata) { return [] } - const { version, credentialSupportedIds } = opts ?? { version: OpenId4VCIVersion.VER_1_0_11 } + const { version } = opts ?? { version: OpenId4VCIVersion.VER_1_0_11 } const usesTransformedCredentialsSupported = version === OpenId4VCIVersion.VER_1_0_08 || !Array.isArray(issuerMetadata.credentials_supported) @@ -31,31 +30,9 @@ export function getSupportedCredentials(opts?: { if (credentialsSupported === undefined || credentialsSupported.length === 0) { return [] - } else if (!credentialSupportedIds || credentialSupportedIds.length === 0) { + } else { return credentialsSupported } - - const credentialSupportedOverlap: CredentialSupported[] = [] - for (const credentialSupportedId of credentialSupportedIds) { - if (typeof credentialSupportedId === 'string') { - const supported = credentialsSupported.find((sup) => { - // Match id to offerType - if (sup.id === credentialSupportedId) return true - - // If the credential was transformed and the v8 variant supported multiple formats for the id, we - // check if there is an id with the format - // see credentialsSupportedV8ToV11 - if (usesTransformedCredentialsSupported && sup.id === `${credentialSupportedId}-${sup.format}`) return true - - return false - }) - if (supported) { - credentialSupportedOverlap.push(supported) - } - } - } - - return credentialSupportedOverlap } export function credentialsSupportedV8ToV11(supportedV8: CredentialSupportedTypeV1_0_08): CredentialSupported[] { diff --git a/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts index 4ae0e33b13..4259f86be0 100644 --- a/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts @@ -109,14 +109,20 @@ describe('OpenId4VcHolder', () => { const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) if (!verificationMethod) throw new Error('No verification method found') - const w3cCredentialRecords = await agent.modules.openId4VcHolder.requestCredentialUsingPreAuthorizedCode({ - issuerUri: fixture.credentialOffer, - verifyCredentialStatus: false, - // We only allow EdDSa, as we've created a did with keyType ed25519. If we create - // or determine the did dynamically we could use any signature algorithm - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - proofOfPossessionVerificationMethodResolver: () => verificationMethod, - }) + const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer( + fixture.credentialOffer + ) + + const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + resolvedCredentialOffer, + { + verifyCredentialStatus: false, + // We only allow EdDSa, as we've created a did with keyType ed25519. If we create + // or determine the did dynamically we could use any signature algorithm + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + proofOfPossessionVerificationMethodResolver: () => verificationMethod, + } + ) expect(w3cCredentialRecords).toHaveLength(1) const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord @@ -166,13 +172,19 @@ describe('OpenId4VcHolder', () => { const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) if (!verificationMethod) throw new Error('No verification method found') - const w3cCredentialRecords = await agent.modules.openId4VcHolder.requestCredentialUsingPreAuthorizedCode({ - issuerUri: fixture.credentialOffer, - allowedCredentialFormats: [OpenIdCredentialFormatProfile.JwtVcJson], - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], - proofOfPossessionVerificationMethodResolver: () => verificationMethod, - verifyCredentialStatus: false, - }) + const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer( + fixture.credentialOffer + ) + + const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + resolvedCredentialOffer, + { + allowedCredentialFormats: [OpenIdCredentialFormatProfile.JwtVcJson], + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], + proofOfPossessionVerificationMethodResolver: () => verificationMethod, + verifyCredentialStatus: false, + } + ) expect(w3cCredentialRecords[0]).toBeInstanceOf(W3cCredentialRecord) const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord @@ -341,16 +353,23 @@ describe('OpenId4VcHolder', () => { scope, initiationUri, }) - const w3cCredentialRecords = await agent.modules.openId4VcHolder.requestCredentialUsingAuthorizationCode({ - clientId: clientId, - authorizationCode: 'test-code', - codeVerifier: codeVerifier, - verifyCredentialStatus: false, - proofOfPossessionVerificationMethodResolver: () => verificationMethod, - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - issuerUri: initiationUri, // TODO - redirectUri: redirectUri, - }) + + const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer( + fixture.credentialOffer + ) + + const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( + resolvedCredentialOffer, + { + clientId: clientId, + authorizationCode: 'test-code', + codeVerifier: codeVerifier, + verifyCredentialStatus: false, + proofOfPossessionVerificationMethodResolver: () => verificationMethod, + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + redirectUri: redirectUri, + } + ) expect(w3cCredentialRecords).toHaveLength(1) const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord @@ -459,14 +478,30 @@ describe('OpenId4VcHolder', () => { const kid = `${didKey.did}#${didKey.key.fingerprint}` const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) if (!verificationMethod) throw new Error('No verification method found') + const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer( + fixture.credentialOffer + ) - const w3cCredentialRecords = await agent.modules.openId4VcHolder.requestCredentialUsingPreAuthorizedCode({ - issuerUri: fixture.credentialOffer, - allowedCredentialFormats: [OpenIdCredentialFormatProfile.JwtVcJson], - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], - proofOfPossessionVerificationMethodResolver: () => verificationMethod, - verifyCredentialStatus: false, - }) + const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + resolvedCredentialOffer, + { + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], + proofOfPossessionVerificationMethodResolver: () => verificationMethod, + verifyCredentialStatus: false, + } + ) + + expect(w3cCredentialRecords[0]).toBeInstanceOf(W3cCredentialRecord) + const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord + + expect(w3cCredentialRecord.credential.type).toEqual([ + 'VerifiableCredential', + 'VerifiableAttestation', + 'VerifiableId', + ]) + + expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) + }) expect(w3cCredentialRecords[0]).toBeInstanceOf(W3cCredentialRecord) const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord From e20fb7be72cad707eec11c7c6af566f900308e2e Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 16 Oct 2023 17:33:56 +0200 Subject: [PATCH 009/115] feat: add more info for credential selection Signed-off-by: Martin Auer --- .../src/OpenId4VcHolderService.ts | 25 ++++++++++++++++--- .../src/OpenId4VcHolderServiceOptions.ts | 16 +++++++++--- .../tests/openid4vc-holder.e2e.test.ts | 1 - 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderService.ts b/packages/openid4vc-holder/src/OpenId4VcHolderService.ts index 08e3af3e67..350909a901 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderService.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderService.ts @@ -1,4 +1,5 @@ import type { + CredentialToRequest, GenerateAuthorizationUrlOptions, ProofOfPossessionRequirements, ProofOfPossessionVerificationMethodResolver, @@ -210,9 +211,26 @@ export class OpenId4VcHolderService { version ) - const credentialsToRequest = offeredCredentialsWithMetadata.map((offeredCredential) => - this.getFormatAndTypesFromOfferedCredential(offeredCredential, version) - ) + const credentialsToRequest: CredentialToRequest[] = offeredCredentialsWithMetadata.map((offeredCredential) => { + const { format, types } = this.getFormatAndTypesFromOfferedCredential(offeredCredential, version) + const offerType = offeredCredential.type + + if (offerType === OfferedCredentialType.InlineCredentialOffer) { + return { offerType, types, format } + } else { + const { id, cryptographic_binding_methods_supported, cryptographic_suites_supported } = + offeredCredential.credentialSupported + + return { + id, + offerType, + cryptographic_binding_methods_supported, + cryptographic_suites_supported, + types, + format, + } + } + }) return { metadata, @@ -248,7 +266,6 @@ export class OpenId4VcHolderService { const allowedCredentialFormats = supportedCredentialFormats - // TODO: how to request specific credentials with the pre-auth flow? // acquire the access token // NOTE: only scope based flow is supported for authorized flow. However there's not clear mapping between // the scope property and which credential to request (this is out of scope of the spec), so it will still diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts index 38fcb44fbc..81cf5c0854 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts @@ -1,3 +1,4 @@ +import type { OfferedCredentialType } from './OpenId4VcHolderService' import type { JwaSignatureAlgorithm, KeyType, VerificationMethod } from '@aries-framework/core' import type { CredentialOfferPayloadV1_0_11, EndpointMetadataResult, OpenId4VCIVersion } from '@sphereon/oid4vci-common' @@ -13,10 +14,17 @@ export const supportedCredentialFormats = [ OpenIdCredentialFormatProfile.LdpVc, ] satisfies OpenIdCredentialFormatProfile[] -export interface CredentialToRequest { - format: string - types: string[] -} +export type CredentialToRequest = { format: string; types: string[] } & ( + | { + offerType: OfferedCredentialType.InlineCredentialOffer + } + | { + offerType: OfferedCredentialType.CredentialSupported + id: string | undefined + cryptographic_binding_methods_supported: string[] | undefined + cryptographic_suites_supported: string[] | undefined + } +) export interface ResolvedCredentialOffer { metadata: EndpointMetadataResult diff --git a/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts index 4259f86be0..2eb7f37787 100644 --- a/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts @@ -179,7 +179,6 @@ describe('OpenId4VcHolder', () => { const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( resolvedCredentialOffer, { - allowedCredentialFormats: [OpenIdCredentialFormatProfile.JwtVcJson], allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], proofOfPossessionVerificationMethodResolver: () => verificationMethod, verifyCredentialStatus: false, From 71d1b73531be25e19ac78a3d30d247e8af150aef Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 16 Oct 2023 17:57:57 +0200 Subject: [PATCH 010/115] fix: copy & paste error Signed-off-by: Martin Auer --- .../tests/openid4vc-holder.e2e.test.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts index 2eb7f37787..b7983fdb90 100644 --- a/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts @@ -14,7 +14,6 @@ import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import nock, { cleanAll, enableNetConnect } from 'nock' import { OpenId4VcHolderModule } from '../src' -import { OpenIdCredentialFormatProfile } from '../src/utils/claimFormatMapping' import { mattrLaunchpadJsonLd_draft_08, @@ -499,18 +498,6 @@ describe('OpenId4VcHolder', () => { 'VerifiableId', ]) - expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) - }) - - expect(w3cCredentialRecords[0]).toBeInstanceOf(W3cCredentialRecord) - const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord - - expect(w3cCredentialRecord.credential.type).toEqual([ - 'VerifiableCredential', - 'VerifiableAttestation', - 'VerifiableId', - ]) - expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) }) }) From 6e2cf46dbb09a10aafa0205ca57d4a60359823a4 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 18 Oct 2023 10:21:48 +0200 Subject: [PATCH 011/115] feat: change public api and others Signed-off-by: Martin Auer --- .../data-integrity/SignatureSuiteRegistry.ts | 2 +- .../src/OpenId4VcHolderApi.ts | 5 - .../src/OpenId4VcHolderService.ts | 177 ++++++------ .../src/OpenId4VcHolderServiceOptions.ts | 1 - packages/openid4vc-holder/tests/fixtures.ts | 60 +++- .../tests/openid4vc-holder.e2e.test.ts | 259 +++++++++++++----- 6 files changed, 337 insertions(+), 167 deletions(-) diff --git a/packages/core/src/modules/vc/data-integrity/SignatureSuiteRegistry.ts b/packages/core/src/modules/vc/data-integrity/SignatureSuiteRegistry.ts index d7cd6e37b4..68a4d5c01e 100644 --- a/packages/core/src/modules/vc/data-integrity/SignatureSuiteRegistry.ts +++ b/packages/core/src/modules/vc/data-integrity/SignatureSuiteRegistry.ts @@ -32,7 +32,7 @@ export class SignatureSuiteRegistry { } public getByKeyType(keyType: KeyType) { - return this.suiteMapping.find((x) => x.keyTypes.includes(keyType)) + return this.suiteMapping.filter((x) => x.keyTypes.includes(keyType)) } public getByProofType(proofType: string) { diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts index 8bd238c031..7f48fdc2e6 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts @@ -25,11 +25,6 @@ export class OpenId4VcHolderApi { this.openId4VcHolderService = openId4VcHolderService } - public async resolveLegacyCredentialOffer(issuerUri: string) { - const resolved = await this.openId4VcHolderService.resolveLegacyCredentialOffer(issuerUri) - return resolved - } - public async resolveCredentialOffer(credentialOffer: string | CredentialOfferPayloadV1_0_11) { const resolved = await this.openId4VcHolderService.resolveCredentialOffer(credentialOffer) return resolved diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderService.ts b/packages/openid4vc-holder/src/OpenId4VcHolderService.ts index 350909a901..1df94189b9 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderService.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderService.ts @@ -23,7 +23,6 @@ import type { Jwt, OpenIDResponse, ProofOfPossessionCallbacks, - UniformCredentialOffer, UniformCredentialOfferPayload, } from '@sphereon/oid4vci-common' @@ -157,13 +156,6 @@ export class OpenId4VcHolderService { } } - public async resolveLegacyCredentialOffer(uri: string) { - const credentialOfferWithBaseUrl = await CredentialOfferClient.fromURI(uri) - const credentialOffer = credentialOfferWithBaseUrl.credential_offer - - return this.resolveCredentialOffer(credentialOffer, { version: credentialOfferWithBaseUrl.version }) - } - private getFormatAndTypesFromOfferedCredential( offeredCredential: OfferedCredentialsWithMetadata, version: OpenId4VCIVersion @@ -183,15 +175,29 @@ export class OpenId4VcHolderService { credentialOffer: UniformCredentialOfferPayload | string, opts?: { version?: OpenId4VCIVersion } ) { - const version = opts?.version ?? OpenId4VCIVersion.VER_1_0_11 - const uniformCredentialOffer: UniformCredentialOffer = { + let version = opts?.version ?? OpenId4VCIVersion.VER_1_0_11 + const claimedCredentialOfferUrl = `openid-credential-offer://?` + const claimedIssuanceInitiaionUrl = `openid-initiate-issuance://?` + + if ( + typeof credentialOffer === 'string' && + (credentialOffer.startsWith(claimedCredentialOfferUrl) || credentialOffer.startsWith(claimedIssuanceInitiaionUrl)) + ) { + const credentialOfferWithBaseUrl = await CredentialOfferClient.fromURI(credentialOffer) + credentialOffer = credentialOfferWithBaseUrl.credential_offer + version = credentialOfferWithBaseUrl.version + } + + const uniformCredentialOffer = { credential_offer: typeof credentialOffer === 'string' ? undefined : credentialOffer, credential_offer_uri: typeof credentialOffer === 'string' ? credentialOffer : undefined, } - const assertedCredentialOffer = (await assertedUniformCredentialOffer(uniformCredentialOffer)).credential_offer - const issuer = assertedCredentialOffer.credential_issuer + + const credentialOfferPayload = (await assertedUniformCredentialOffer(uniformCredentialOffer)).credential_offer + const issuer = credentialOfferPayload.credential_issuer const metadata = await MetadataClient.retrieveAllMetadata(issuer) + if (!metadata) throw new AriesFrameworkError(`Could not retrieve metadata for OpenID4VCI issuer: ${issuer}`) this.logger.info('Fetched server metadata', { issuer: metadata.issuer, @@ -201,12 +207,8 @@ export class OpenId4VcHolderService { this.logger.debug('Full server metadata', metadata) - if (!metadata) { - throw new AriesFrameworkError(`Could not retrieve metadata for OpenID4VCI issuer: ${issuer}`) - } - const offeredCredentialsWithMetadata = this.getOfferedCredentialsWithMetadata( - assertedCredentialOffer, + credentialOfferPayload, metadata.credentialIssuerMetadata, version ) @@ -221,20 +223,13 @@ export class OpenId4VcHolderService { const { id, cryptographic_binding_methods_supported, cryptographic_suites_supported } = offeredCredential.credentialSupported - return { - id, - offerType, - cryptographic_binding_methods_supported, - cryptographic_suites_supported, - types, - format, - } + return { id, offerType, cryptographic_binding_methods_supported, cryptographic_suites_supported, types, format } } }) return { metadata, - credentialOfferPayload: assertedCredentialOffer, + credentialOfferPayload, credentialsToRequest, version, } @@ -242,6 +237,11 @@ export class OpenId4VcHolderService { public async acceptCredentialOffer(agentContext: AgentContext, options: RequestCredentialOptions) { const { credentialsToRequest, credentialOfferPayload, metadata, version } = options + this.logger.info(`Accepting the following credential offers '${credentialsToRequest}'`) + if (credentialsToRequest?.length === 0) { + this.logger.warn(`Accepting 0 credential offers. Returning`) + return [] + } const flowType = flowTypeMapping[options.flowType] if (!flowType) { @@ -252,6 +252,7 @@ export class OpenId4VcHolderService { const supportedJwaSignatureAlgorithms = this.getSupportedJwaSignatureAlgorithms(agentContext) + // TODO: do we want to change this? const allowedProofOfPossessionSignatureAlgorithms = options.allowedProofOfPossessionSignatureAlgorithms ? options.allowedProofOfPossessionSignatureAlgorithms.filter((algorithm) => supportedJwaSignatureAlgorithms.includes(algorithm) @@ -262,10 +263,6 @@ export class OpenId4VcHolderService { throw new AriesFrameworkError(`No supported proof of possession signature algorithms found.`) } - const receivedCredentials: W3cCredentialRecord[] = [] - - const allowedCredentialFormats = supportedCredentialFormats - // acquire the access token // NOTE: only scope based flow is supported for authorized flow. However there's not clear mapping between // the scope property and which credential to request (this is out of scope of the spec), so it will still @@ -294,6 +291,8 @@ export class OpenId4VcHolderService { if (!openIdAccessTokenResponse.successBody) { throw new AriesFrameworkError(`could not acquire access token from '${metadata.issuer}'`) } + this.logger.debug('Requested OpenId4VCI Access Token') + const accessToken = openIdAccessTokenResponse.successBody const issuerMetadata = metadata.credentialIssuerMetadata @@ -319,15 +318,15 @@ export class OpenId4VcHolderService { return credentialToRequest }) - // Loop through all the credentialTypes in the credential offer - for (const offeredCredential of credentialsToRequestWithMetadata ?? offeredCredentialsWithMetadata) { - const isInlineOffer = isInlineCredentialOffer(offeredCredential) + const receivedCredentials: W3cCredentialRecord[] = [] + // Loop through all the credentialTypes in the credential offer + for (const credentialWithMetadata of credentialsToRequestWithMetadata ?? offeredCredentialsWithMetadata) { // Get all options for the credential request (such as which kid to use, the signature algorithm, etc) const { verificationMethod, signatureAlgorithm } = await this.getCredentialRequestOptions(agentContext, { - allowedCredentialFormats, + allowedCredentialFormats: supportedCredentialFormats, allowedProofOfPossessionSignatureAlgorithms, - offeredCredentialWithMetadata: offeredCredential, + offeredCredentialWithMetadata: credentialWithMetadata, proofOfPossessionVerificationMethodResolver: options.proofOfPossessionVerificationMethodResolver, }) @@ -356,33 +355,32 @@ export class OpenId4VcHolderService { .withCredentialEndpoint(metadata.credential_endpoint) .withTokenFromResponse(accessToken) + const isInlineOffer = isInlineCredentialOffer(credentialWithMetadata) + + const format = isInlineOffer + ? credentialWithMetadata.inlineCredentialOffer.format + : credentialWithMetadata.credentialSupported.format + let credentialTypes: string | string[] if (version < OpenId4VCIVersion.VER_1_0_11) { if (isInlineOffer) throw new AriesFrameworkError(`Inline credential offers not supported for version < 11`) - // TODO: this is wrong, how can we determine the credential types for v8? If more then 1 type is provided? - credentialTypes = offeredCredential.credentialSupported.types + if (!credentialWithMetadata.credentialSupported.id) { + throw new AriesFrameworkError( // This should not happen + `No id provided for a credential supported entry in combination with the OpenId4VCI v8 draft` + ) + } + credentialTypes = credentialWithMetadata.credentialSupported.id.split(`-${format}`)[0] } else { - if (isInlineOffer) credentialTypes = offeredCredential.inlineCredentialOffer.types - else credentialTypes = offeredCredential.credentialSupported.types + if (isInlineOffer) credentialTypes = credentialWithMetadata.inlineCredentialOffer.types + else credentialTypes = credentialWithMetadata.credentialSupported.types } const credentialRequestClient = credentialRequestBuilder.build() - - let credentialResponse: OpenIDResponse - - if (isInlineCredentialOffer(offeredCredential)) { - credentialResponse = await credentialRequestClient.acquireCredentialsUsingProof({ - proofInput, - credentialTypes, - format: offeredCredential.inlineCredentialOffer.format, - }) - } else { - credentialResponse = await credentialRequestClient.acquireCredentialsUsingProof({ - proofInput, - credentialTypes, - format: offeredCredential.credentialSupported.format, - }) - } + const credentialResponse = await credentialRequestClient.acquireCredentialsUsingProof({ + proofInput, + credentialTypes, + format, + }) const credential = await this.handleCredentialResponse(agentContext, credentialResponse, { verifyCredentialStatus: options.verifyCredentialStatus, @@ -397,8 +395,9 @@ export class OpenId4VcHolderService { }) this.logger.debug('Full credential', credentialRecord) - if (!isInlineCredentialOffer(offeredCredential)) { - const supportedCredentialMetadata = offeredCredential.credentialSupported + // TODO: what to do with this? + if (!isInlineCredentialOffer(credentialWithMetadata)) { + const supportedCredentialMetadata = credentialWithMetadata.credentialSupported // Set the OpenId4Vc credential metadata and update record setOpenId4VcCredentialMetadata(credentialRecord, supportedCredentialMetadata, metadata, issuerMetadata) } @@ -549,42 +548,39 @@ export class OpenId4VcHolderService { private getProofOfPossessionRequirements( agentContext: AgentContext, options: { - allowedCredentialFormats: SupportedCredentialFormats[] offeredCredentialWithMetadata: OfferedCredentialsWithMetadata + allowedCredentialFormats: SupportedCredentialFormats[] allowedProofOfPossessionSignatureAlgorithms: JwaSignatureAlgorithm[] } ): ProofOfPossessionRequirements { const { offeredCredentialWithMetadata, allowedCredentialFormats } = options + const isInlineOffer = offeredCredentialWithMetadata.type === OfferedCredentialType.InlineCredentialOffer // Extract format from offer - let format = - offeredCredentialWithMetadata.type === OfferedCredentialType.InlineCredentialOffer - ? offeredCredentialWithMetadata.inlineCredentialOffer.format - : offeredCredentialWithMetadata.credentialSupported.format + let format = isInlineOffer + ? offeredCredentialWithMetadata.inlineCredentialOffer.format + : offeredCredentialWithMetadata.credentialSupported.format // Get uniform format, so we don't have to deal with the different spec versions format = getUniformFormat(format) - const credentialMetadata = - offeredCredentialWithMetadata.type === OfferedCredentialType.CredentialSupported - ? offeredCredentialWithMetadata.credentialSupported - : undefined + const credentialSupportedMetadata = isInlineOffer ? undefined : offeredCredentialWithMetadata.credentialSupported - const issuerSupportedCryptographicSuites = credentialMetadata?.cryptographic_suites_supported + const issuerSupportedCryptographicSuites = credentialSupportedMetadata?.cryptographic_suites_supported const issuerSupportedBindingMethods = - credentialMetadata?.cryptographic_binding_methods_supported ?? + credentialSupportedMetadata?.cryptographic_binding_methods_supported ?? // FIXME: somehow the MATTR Launchpad returns binding_methods_supported instead of cryptographic_binding_methods_supported // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - (credentialMetadata?.binding_methods_supported as string[] | undefined) + (credentialSupportedMetadata?.binding_methods_supported as string[] | undefined) - if (!isInlineCredentialOffer(offeredCredentialWithMetadata)) { + if (!isInlineOffer) { const credentialMetadata = offeredCredentialWithMetadata.credentialSupported if (!allowedCredentialFormats.includes(format as SupportedCredentialFormats)) { throw new AriesFrameworkError( - `Issuer only supports format '${format}' for credential type '${ - credentialMetadata.id as string - }', but the wallet only allows formats '${options.allowedCredentialFormats.join(', ')}'` + `Issuer only supports format '${format}' for credential type '${credentialMetadata.types.join( + ', ' + )}', but the wallet only allows formats '${options.allowedCredentialFormats.join(', ')}'` ) } } @@ -619,11 +615,10 @@ export class OpenId4VcHolderService { const JwkClass = getJwkClassFromJwaSignatureAlgorithm(signatureAlgorithm) if (!JwkClass) return false - // TODO: getByKeyType should return a list const matchingSuite = signatureSuiteRegistry.getByKeyType(JwkClass.keyType) - if (!matchingSuite) return false + if (matchingSuite.length === 0) return false - return issuerSupportedCryptographicSuites.includes(matchingSuite.proofType) + return issuerSupportedCryptographicSuites.includes(matchingSuite[0].proofType) } ) } @@ -631,7 +626,7 @@ export class OpenId4VcHolderService { default: throw new AriesFrameworkError( `Unsupported requested credential format '${format}' with id ${ - credentialMetadata?.id ?? 'Inline credential offer' + credentialSupportedMetadata?.id ?? 'Inline credential offer' }` ) } @@ -642,7 +637,7 @@ export class OpenId4VcHolderService { if (!potentialSignatureAlgorithm) { throw new AriesFrameworkError( `Could not establish signature algorithm for format ${format} and id ${ - credentialMetadata?.id ?? 'Inline credential offer' + credentialSupportedMetadata?.id ?? 'Inline credential offer' }` ) } @@ -672,9 +667,7 @@ export class OpenId4VcHolderService { // Filter out the undefined values .filter((jwkClass): jwkClass is Exclude => jwkClass !== undefined) // Extract the supported JWA signature algorithms from the JWK class - .map((jwkClass) => jwkClass.supportedSignatureAlgorithms) - // Flatten the array of arrays - .reduce((allAlgorithms, algorithms) => [...allAlgorithms, ...algorithms], []) + .flatMap((jwkClass) => jwkClass.supportedSignatureAlgorithms) return supportedJwaSignatureAlgorithms } @@ -712,9 +705,7 @@ export class OpenId4VcHolderService { } if (!result || !result.isValid) { - agentContext.config.logger.error('Failed to validate credential', { - result, - }) + agentContext.config.logger.error('Failed to validate credential', { result }) throw new AriesFrameworkError(`Failed to validate credential, error = ${result.error?.message ?? 'Unknown'}`) } @@ -723,17 +714,9 @@ export class OpenId4VcHolderService { private signCallback(agentContext: AgentContext, verificationMethod: VerificationMethod) { return async (jwt: Jwt, kid?: string) => { - if (!jwt.header) { - throw new AriesFrameworkError('No header present on JWT') - } - - if (!jwt.payload) { - throw new AriesFrameworkError('No payload present on JWT') - } - - if (!kid) { - throw new AriesFrameworkError('No KID is present in the callback') - } + if (!jwt.header) throw new AriesFrameworkError('No header present on JWT') + if (!jwt.payload) throw new AriesFrameworkError('No payload present on JWT') + if (!kid) throw new AriesFrameworkError('No KID is present in the callback') // We have determined the verification method before and already passed that when creating the callback, // however we just want to make sure that the kid matches the verification method id @@ -743,14 +726,14 @@ export class OpenId4VcHolderService { const key = getKeyFromVerificationMethod(verificationMethod) const jwk = getJwkFromKey(key) - - const payload = JsonEncoder.toBuffer(jwt.payload) if (!jwk.supportsSignatureAlgorithm(jwt.header.alg)) { throw new AriesFrameworkError( `kid ${kid} refers to a key of type '${jwk.keyType}', which does not support the JWS signature alg '${jwt.header.alg}'` ) } + const payload = JsonEncoder.toBuffer(jwt.payload) + // We don't support these properties, remove them, so we can pass all other header properties to the JWS service if (jwt.header.x5c || jwt.header.jwk) throw new AriesFrameworkError('x5c and jwk are not supported') // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts index 81cf5c0854..6f7d0d935b 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts @@ -198,7 +198,6 @@ type WithInternalOptions = Options & { * The endpoint metadata received from the credential issuer. * This is obtained manually or by calling the `resolveCredentialOffer` method. */ - // TODO: reduce this to contain only the issuer metdata // metadata: EndpointMetadataResult /** diff --git a/packages/openid4vc-holder/tests/fixtures.ts b/packages/openid4vc-holder/tests/fixtures.ts index 54e46bb496..a71c9f2782 100644 --- a/packages/openid4vc-holder/tests/fixtures.ts +++ b/packages/openid4vc-holder/tests/fixtures.ts @@ -330,7 +330,7 @@ export const waltIdJffJwt_draft_08 = { // This object is MANUALLY converted and should be updated when we have actual test vectors export const waltIdJffJwt_draft_11 = { credentialOffer: - 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fjff.walt.id%2Fissuer-api%2Fdefault%2Foidc%22%2C%22credentials%22%3A%5B%22VerifiableId%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22ABC%22%7D%7D%7D', + 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fjff.walt.id%2Fissuer-api%2Fdefault%2Foidc%22%2C%22credentials%22%3A%5B%22VerifiableId%22%2C%20%22VerifiableDiploma%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22ABC%22%7D%7D%7D', getMetadataResponse: { authorization_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/fulfillPAR', token_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/token', @@ -347,7 +347,7 @@ export const waltIdJffJwt_draft_11 = { format: 'jwt_vc_json', cryptographic_binding_methods_supported: ['did'], cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], - types: ['VerifiableCredential', 'OpenBadgeCredential'], + types: ['VerifiableCredential', 'VerifiableId'], }, { id: 'VerifiableDiploma', @@ -457,4 +457,60 @@ export const waltIdJffJwt_draft_11 = { 'eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCMwIiwidHlwIjoiSldUIiwiYWxnIjoiRWREU0EifQ.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCIsInN1YiI6ImRpZDprZXk6ekRuYWVpcFdnOURNWFB0OWpjbUFCcWFZUlZLYzE5dFgxeGZCUldGc0pTUG9VZE1udiIsIm5iZiI6MTY4NTM1MDc4OSwiaWF0IjoxNjg1MzUwNzg5LCJ2YyI6eyJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVmVyaWZpYWJsZUF0dGVzdGF0aW9uIiwiVmVyaWZpYWJsZUlkIl0sIkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sImlkIjoidXJuOnV1aWQ6NTljZTRhYzItZWM2NS00YjhmLThmOTYtZWE3ODUxMmRmOWQzIiwiaXNzdWVyIjoiZGlkOmp3azpleUpyZEhraU9pSlBTMUFpTENKMWMyVWlPaUp6YVdjaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWEybGtJam9pTjJRMlkySm1NalE0T1dJek5ESTNObUl4TnpJeE9UQTFORGxrTWpNNU1UZ2lMQ0o0SWpvaVJtNUZWVlZoZFdSdE9UbE9NekJpT0RCcWN6aFdkRFJCYms5NGRsSjNXSFJuVW1OTGNUTm5Ra2wxT0NJc0ltRnNaeUk2SWtWa1JGTkJJbjAiLCJpc3N1YW5jZURhdGUiOiIyMDIzLTA1LTI5VDA4OjU5OjQ5WiIsImlzc3VlZCI6IjIwMjMtMDUtMjlUMDg6NTk6NDlaIiwidmFsaWRGcm9tIjoiMjAyMy0wNS0yOVQwODo1OTo0OVoiLCJjcmVkZW50aWFsU2NoZW1hIjp7ImlkIjoiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3dhbHQtaWQvd2FsdGlkLXNzaWtpdC12Y2xpYi9tYXN0ZXIvc3JjL3Rlc3QvcmVzb3VyY2VzL3NjaGVtYXMvVmVyaWZpYWJsZUlkLmpzb24iLCJ0eXBlIjoiRnVsbEpzb25TY2hlbWFWYWxpZGF0b3IyMDIxIn0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6RG5hZWlwV2c5RE1YUHQ5amNtQUJxYVlSVktjMTl0WDF4ZkJSV0ZzSlNQb1VkTW52IiwiY3VycmVudEFkZHJlc3MiOlsiMSBCb3VsZXZhcmQgZGUgbGEgTGliZXJ0w6ksIDU5ODAwIExpbGxlIl0sImRhdGVPZkJpcnRoIjoiMTk5My0wNC0wOCIsImZhbWlseU5hbWUiOiJET0UiLCJmaXJzdE5hbWUiOiJKYW5lIiwiZ2VuZGVyIjoiRkVNQUxFIiwibmFtZUFuZEZhbWlseU5hbWVBdEJpcnRoIjoiSmFuZSBET0UiLCJwZXJzb25hbElkZW50aWZpZXIiOiIwOTA0MDA4MDg0SCIsInBsYWNlT2ZCaXJ0aCI6IkxJTExFLCBGUkFOQ0UifSwiZXZpZGVuY2UiOlt7ImRvY3VtZW50UHJlc2VuY2UiOlsiUGh5c2ljYWwiXSwiZXZpZGVuY2VEb2N1bWVudCI6WyJQYXNzcG9ydCJdLCJzdWJqZWN0UHJlc2VuY2UiOiJQaHlzaWNhbCIsInR5cGUiOlsiRG9jdW1lbnRWZXJpZmljYXRpb24iXSwidmVyaWZpZXIiOiJkaWQ6ZWJzaToyQTlCWjlTVWU2QmF0YWNTcHZzMVY1Q2RqSHZMcFE3YkVzaTJKYjZMZEhLblF4YU4ifV19LCJqdGkiOiJ1cm46dXVpZDo1OWNlNGFjMi1lYzY1LTRiOGYtOGY5Ni1lYTc4NTEyZGY5ZDMifQ.6Wn8X2tEQJ9CmX3-meCxDuGmevRdtivnjVkGPXzfnJ-1M6AU4SFxxon0JmMjdmO_h4P9sCEe9RTtyTJou2yeCA', format: 'jwt_vc', }, + + jsonLdCredentialResponse: { + format: 'ldp_vc', + credential: { + type: ['VerifiableCredential', 'PermanentResidentCard'], + issuer: { + id: 'did:web:launchpad.vii.electron.mattrlabs.io', + name: 'Government of Kakapo', + logoUrl: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/logo.svg', + iconUrl: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/icon.svg', + image: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/icon.svg', + }, + name: 'Permanent Resident Card', + description: 'Government of Kakapo', + credentialBranding: { + backgroundColor: '#3a2d2d', + watermarkImageUrl: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/watermark@2x.png', + }, + issuanceDate: '2023-10-17T14:27:36.909Z', + credentialSubject: { + id: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + type: ['PermanentResident', 'Person'], + image: + 'data:image/png;base64,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', + gender: 'Male', + birthDate: '1958-08-17', + givenName: 'Louis', + lprNumber: '1958-08-17', + familyName: 'Pasteur', + lprCategory: 'C09', + birthCountry: 'France', + residentSince: '2015-01-01', + commuterClassification: 'C1', + }, + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://mattr.global/contexts/vc-extensions/v2', + 'https://w3id.org/citizenship/v1', + 'https://w3id.org/vc-revocation-list-2020/v1', + ], + credentialStatus: { + id: 'https://launchpad.vii.electron.mattrlabs.io/core/v1/revocation-lists/7bc7c021-56ee-445a-9143-fd79629df2aa#657', + type: 'RevocationList2020Status', + revocationListIndex: '657', + revocationListCredential: + 'https://launchpad.vii.electron.mattrlabs.io/core/v1/revocation-lists/7bc7c021-56ee-445a-9143-fd79629df2aa', + }, + proof: { + type: 'Ed25519Signature2018', + created: '2023-10-17T14:27:38Z', + jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..BaAHvbYzaFHp5rcqBChGVDd1gBpb9ezD4Rxn-Ev7uP1Jj71OfpcLH-oivuV90OGxgghaRwPe6rnBjwwo-RBjDg', + proofPurpose: 'assertionMethod', + verificationMethod: 'did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg', + }, + }, + }, } diff --git a/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts index b7983fdb90..46e1f842d7 100644 --- a/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts @@ -8,12 +8,13 @@ import { TypedArrayEncoder, W3cCredentialRecord, DidKey, + ClaimFormat, } from '@aries-framework/core' import { agentDependencies } from '@aries-framework/node' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import nock, { cleanAll, enableNetConnect } from 'nock' -import { OpenId4VcHolderModule } from '../src' +import { OpenId4VcHolderModule, OpenIdCredentialFormatProfile } from '../src' import { mattrLaunchpadJsonLd_draft_08, @@ -36,10 +37,10 @@ describe('OpenId4VcHolder', () => { beforeEach(async () => { agent = new Agent({ config: { - label: 'OpenId4VcHolder Test', + label: 'OpenId4VcHolder Test10', walletConfig: { - id: 'openid4vc-holder-test', - key: 'openid4vc-holder-test', + id: 'openid4vc-holder-test11', + key: 'openid4vc-holder-test12', }, }, dependencies: agentDependencies, @@ -181,6 +182,9 @@ describe('OpenId4VcHolder', () => { allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], proofOfPossessionVerificationMethodResolver: () => verificationMethod, verifyCredentialStatus: false, + credentialsToRequest: resolvedCredentialOffer.credentialsToRequest.filter((credential) => { + return credential.format === OpenIdCredentialFormatProfile.JwtVcJson + }), } ) @@ -389,66 +393,170 @@ describe('OpenId4VcHolder', () => { enableNetConnect() }) - // it('[DRAFT 11]: Should successfully execute the pre-authorized flow using a did:key Ed25519 subject and JSON-LD credential', async () => { - // const fixture = waltIdJffJwt_draft_11 + it('[DRAFT 11]: Should successfully execute the pre-authorized if no credential is requested', async () => { + const fixture = waltIdJffJwt_draft_11 - // nock('https://jff.walt.id/issuer-api/default/oidc') - // .get('/.well-known/openid-credential-issuer') - // .reply(200, fixture.getMetadataResponse) + /** + * Below we're setting up some mock HTTP responses. + * These responses are based on the openid-initiate-issuance URI above + */ + // setup server metadata response + nock('https://jff.walt.id/issuer-api/default/oidc') + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.getMetadataResponse) + .get('/.well-known/openid-configuration') + .reply(404) + .get('/.well-known/oauth-authorization-server') + .reply(404) - // // setup access token response - // .post('/token') - // .reply(200, fixture.credentialResponse) + const did = await agent.dids.create({ + method: 'key', + options: { keyType: KeyType.P256 }, + secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, + }) - // // setup credential request response - // .post('/credential') - // .reply(200, fixture.credentialResponse) + const didKey = DidKey.fromDid(did.didState.did as string) + const kid = `${didKey.did}#${didKey.key.fingerprint}` + const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) + if (!verificationMethod) throw new Error('No verification method found') - // const did = await agent.dids.create({ - // method: 'key', - // options: { - // keyType: KeyType.Ed25519, - // }, - // secret: { - // privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), - // }, - // }) + const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer( + fixture.credentialOffer + ) - // const didKey = DidKey.fromDid(did.didState.did as string) - // const kid = `${did.didState.did as string}#${didKey.key.fingerprint}` - // const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) - // if (!verificationMethod) throw new Error('No verification method found') + const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + resolvedCredentialOffer, + { + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], + proofOfPossessionVerificationMethodResolver: () => verificationMethod, + verifyCredentialStatus: false, + credentialsToRequest: [], + } + ) - // const w3cCredentialRecords = await agent.modules.openId4VcClient.requestCredentialUsingPreAuthorizedCode({ - // uri: fixture.credentialOffer, - // verifyCredentialStatus: false, - // // We only allow EdDSa, as we've created a did with keyType ed25519. If we create - // // or determine the did dynamically we could use any signature algorithm - // allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - // proofOfPossessionVerificationMethodResolver: () => verificationMethod, - // }) + expect(w3cCredentialRecords).toHaveLength(0) + }) - // expect(w3cCredentialRecords).toHaveLength(1) - // const w3cCredentialRecord = w3cCredentialRecords[0] - // expect(w3cCredentialRecord).toBeInstanceOf(W3cCredentialRecord) + it('[DRAFT 11]: Should successfully execute the pre-authorized flow using a single offered credential a did:key ES256 subject and JwtVc format', async () => { + const fixture = waltIdJffJwt_draft_11 + const httpMock = nock('https://jff.walt.id/issuer-api/default/oidc') + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.getMetadataResponse) + .get('/.well-known/openid-configuration') + .reply(404) + .get('/.well-known/oauth-authorization-server') + .reply(404) + + // setup access token response + httpMock.post('/token').reply(200, fixture.acquireAccessTokenResponse) + // setup credential request response + httpMock.post('/credential').reply(200, fixture.credentialResponse) - // expect(w3cCredentialRecord.credential.type).toEqual([ - // 'VerifiableCredential', - // 'VerifiableAttestation', - // 'VerifiableId', - // ]) + const did = await agent.dids.create({ + method: 'key', + options: { keyType: KeyType.P256 }, + secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, + }) - // expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) - // }) + const didKey = DidKey.fromDid(did.didState.did as string) + const kid = `${didKey.did}#${didKey.key.fingerprint}` + const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) + if (!verificationMethod) throw new Error('No verification method found') + const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer( + fixture.credentialOffer + ) + + expect(resolvedCredentialOffer.credentialsToRequest).toHaveLength(2) + const selectedCredentialsForRequest = resolvedCredentialOffer.credentialsToRequest.filter((credential) => { + return ( + credential.format === OpenIdCredentialFormatProfile.JwtVcJson && credential.types.includes('VerifiableId') + ) + }) + + expect(selectedCredentialsForRequest).toHaveLength(1) + + const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + resolvedCredentialOffer, + { + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], + proofOfPossessionVerificationMethodResolver: () => verificationMethod, + verifyCredentialStatus: false, + credentialsToRequest: selectedCredentialsForRequest, + } + ) + + expect(w3cCredentialRecords).toHaveLength(1) + expect(w3cCredentialRecords[0]).toBeInstanceOf(W3cCredentialRecord) + const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord - it('[DRAFT 11]: Should successfully execute the pre-authorized flow using a did:key P256 subject and JWT credential', async () => { + expect(w3cCredentialRecord.credential.type).toEqual([ + 'VerifiableCredential', + 'VerifiableAttestation', + 'VerifiableId', + ]) + + expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) + }) + + it('[DRAFT 11]: Should successfully execute the pre-authorized flow using a single offered credential a did:key EdDSA subject and JsonLd format', async () => { const fixture = waltIdJffJwt_draft_11 + const httpMock = nock('https://jff.walt.id/issuer-api/default/oidc') + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.getMetadataResponse) + .get('/.well-known/openid-configuration') + .reply(404) + .get('/.well-known/oauth-authorization-server') + .reply(404) - /** - * Below we're setting up some mock HTTP responses. - * These responses are based on the openid-initiate-issuance URI above - */ - // setup server metadata response + // setup access token response + httpMock.post('/token').reply(200, fixture.acquireAccessTokenResponse) + // setup credential request response + httpMock.post('/credential').reply(200, fixture.jsonLdCredentialResponse) + + const did = await agent.dids.create({ + method: 'key', + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, + }) + + const didKey = DidKey.fromDid(did.didState.did as string) + const kid = `${didKey.did}#${didKey.key.fingerprint}` + const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) + if (!verificationMethod) throw new Error('No verification method found') + const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer( + fixture.credentialOffer + ) + + expect(resolvedCredentialOffer.credentialsToRequest).toHaveLength(2) + const selectedCredentialsForRequest = resolvedCredentialOffer.credentialsToRequest.filter((credential) => { + return ( + credential.format === OpenIdCredentialFormatProfile.LdpVc && credential.types.includes('VerifiableDiploma') + ) + }) + + expect(selectedCredentialsForRequest).toHaveLength(1) + + const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + resolvedCredentialOffer, + { + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + proofOfPossessionVerificationMethodResolver: () => verificationMethod, + verifyCredentialStatus: false, + credentialsToRequest: selectedCredentialsForRequest, + } + ) + + expect(w3cCredentialRecords).toHaveLength(1) + expect(w3cCredentialRecords[0]).toBeInstanceOf(W3cCredentialRecord) + const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord + + expect(w3cCredentialRecord.credential.type).toEqual(['VerifiableCredential', 'PermanentResidentCard']) + + expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) + }) + + it('[DRAFT 11]: Should successfully execute the pre-authorized for multiple credentials of different formats using a did:key EdDsa subject', async () => { + const fixture = waltIdJffJwt_draft_11 const httpMock = nock('https://jff.walt.id/issuer-api/default/oidc') .get('/.well-known/openid-credential-issuer') .reply(200, fixture.getMetadataResponse) @@ -461,15 +569,12 @@ describe('OpenId4VcHolder', () => { httpMock.post('/token').reply(200, fixture.credentialResponse) // setup credential request response httpMock.post('/credential').reply(200, fixture.credentialResponse) + httpMock.post('/credential').reply(200, fixture.jsonLdCredentialResponse) const did = await agent.dids.create({ method: 'key', - options: { - keyType: KeyType.P256, - }, - secret: { - privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), - }, + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, }) const didKey = DidKey.fromDid(did.didState.did as string) @@ -480,25 +585,57 @@ describe('OpenId4VcHolder', () => { fixture.credentialOffer ) + expect(resolvedCredentialOffer.credentialsToRequest).toHaveLength(2) + const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( resolvedCredentialOffer, { - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], proofOfPossessionVerificationMethodResolver: () => verificationMethod, verifyCredentialStatus: false, } ) + expect(w3cCredentialRecords.length).toEqual(2) expect(w3cCredentialRecords[0]).toBeInstanceOf(W3cCredentialRecord) const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord - + expect(w3cCredentialRecord.credential.claimFormat).toEqual(ClaimFormat.JwtVc) expect(w3cCredentialRecord.credential.type).toEqual([ 'VerifiableCredential', 'VerifiableAttestation', 'VerifiableId', ]) - expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) + expect(w3cCredentialRecords[1]).toBeInstanceOf(W3cCredentialRecord) + const w3cCredentialRecord1 = w3cCredentialRecords[1] as W3cCredentialRecord + expect(w3cCredentialRecord1.credential.claimFormat).toEqual(ClaimFormat.LdpVc) + expect(w3cCredentialRecord1.credential.type).toEqual(['VerifiableCredential', 'PermanentResidentCard']) + expect(w3cCredentialRecord1.credential.credentialSubjectIds[0]).toEqual(did.didState.did) }) + + // it('use with jff / mattr demo', async () => { + // const did = await agent.dids.create({ + // method: 'key', + // options: { keyType: KeyType.Ed25519 }, + // secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, + // }) + + // const didKey = DidKey.fromDid(did.didState.did as string) + // const kid = `${didKey.did}#${didKey.key.fingerprint}` + // const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) + // if (!verificationMethod) throw new Error('No verification method found') + // const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer( + // `openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22PermanentResidentCard%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22t4y9cy-vBivTynmXuoPVT6cB8YIOo5mU-yKRUpx4vxB%22%7D%7D%7D` + // ) + + // const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + // resolvedCredentialOffer, + // { + // allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + // proofOfPossessionVerificationMethodResolver: () => verificationMethod, + // verifyCredentialStatus: false, + // } + // ) + // }) }) }) From 8a7906972d8ed3159ef3370dc3bddd5a2256bb2b Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 18 Oct 2023 11:11:46 +0200 Subject: [PATCH 012/115] fix: remove some todos Signed-off-by: Martin Auer --- packages/openid4vc-holder/src/OpenId4VcHolderService.ts | 1 - packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderService.ts b/packages/openid4vc-holder/src/OpenId4VcHolderService.ts index 1df94189b9..48214f813c 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderService.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderService.ts @@ -252,7 +252,6 @@ export class OpenId4VcHolderService { const supportedJwaSignatureAlgorithms = this.getSupportedJwaSignatureAlgorithms(agentContext) - // TODO: do we want to change this? const allowedProofOfPossessionSignatureAlgorithms = options.allowedProofOfPossessionSignatureAlgorithms ? options.allowedProofOfPossessionSignatureAlgorithms.filter((algorithm) => supportedJwaSignatureAlgorithms.includes(algorithm) diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts index 6f7d0d935b..c6c05fd761 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts @@ -133,7 +133,6 @@ export interface ProofOfPossessionVerificationMethodResolverOptions { * If the offered credential is an inline credential offer, the value * will be `undefined`. */ - // TODO: do we need credentialType here? supportedCredentialId?: string /** From f7d7904c2dbe828414bc5521ac134875a33b5024 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 18 Oct 2023 11:23:42 +0200 Subject: [PATCH 013/115] fix: remove credential metadata Signed-off-by: Martin Auer --- .../src/OpenId4VcHolderService.ts | 9 +-- packages/openid4vc-holder/src/index.ts | 7 +- packages/openid4vc-holder/src/utils/index.ts | 1 - .../openid4vc-holder/src/utils/metadata.ts | 69 ------------------- 4 files changed, 2 insertions(+), 84 deletions(-) delete mode 100644 packages/openid4vc-holder/src/utils/metadata.ts diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderService.ts b/packages/openid4vc-holder/src/OpenId4VcHolderService.ts index 48214f813c..2e3802f2c4 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderService.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderService.ts @@ -68,7 +68,7 @@ import { import { randomStringForEntropy } from '@stablelib/random' import { AuthFlowType, supportedCredentialFormats } from './OpenId4VcHolderServiceOptions' -import { fromOpenIdCredentialFormatProfileToDifClaimFormat, setOpenId4VcCredentialMetadata } from './utils' +import { fromOpenIdCredentialFormatProfileToDifClaimFormat } from './utils' import { getUniformFormat } from './utils/Formats' import { getSupportedCredentials } from './utils/IssuerMetadataUtils' @@ -394,13 +394,6 @@ export class OpenId4VcHolderService { }) this.logger.debug('Full credential', credentialRecord) - // TODO: what to do with this? - if (!isInlineCredentialOffer(credentialWithMetadata)) { - const supportedCredentialMetadata = credentialWithMetadata.credentialSupported - // Set the OpenId4Vc credential metadata and update record - setOpenId4VcCredentialMetadata(credentialRecord, supportedCredentialMetadata, metadata, issuerMetadata) - } - receivedCredentials.push(credentialRecord) } diff --git a/packages/openid4vc-holder/src/index.ts b/packages/openid4vc-holder/src/index.ts index b345b161b0..e1fea1adac 100644 --- a/packages/openid4vc-holder/src/index.ts +++ b/packages/openid4vc-holder/src/index.ts @@ -14,9 +14,4 @@ export { SupportedCredentialFormats, } from './OpenId4VcHolderServiceOptions' export * from './presentations' -export { - getOpenId4VcCredentialMetadata, - OpenId4VcCredentialMetadata, - OpenIdCredentialFormatProfile, - setOpenId4VcCredentialMetadata, -} from './utils' +export { OpenIdCredentialFormatProfile } from './utils' diff --git a/packages/openid4vc-holder/src/utils/index.ts b/packages/openid4vc-holder/src/utils/index.ts index ee55f476ed..425dcebc12 100644 --- a/packages/openid4vc-holder/src/utils/index.ts +++ b/packages/openid4vc-holder/src/utils/index.ts @@ -1,2 +1 @@ export * from './claimFormatMapping' -export * from './metadata' diff --git a/packages/openid4vc-holder/src/utils/metadata.ts b/packages/openid4vc-holder/src/utils/metadata.ts deleted file mode 100644 index 2ae316632b..0000000000 --- a/packages/openid4vc-holder/src/utils/metadata.ts +++ /dev/null @@ -1,69 +0,0 @@ -import type { W3cCredentialRecord } from '@aries-framework/core' -import type { - CredentialIssuerMetadata, - CredentialsSupportedDisplay, - CredentialSupported, - EndpointMetadata, - IssuerCredentialSubject, - IssuerMetadataV1_0_08, - MetadataDisplay, -} from '@sphereon/oid4vci-common' - -export interface OpenId4VcCredentialMetadata { - credential: { - display?: CredentialsSupportedDisplay[] - order?: string[] - credentialSubject: IssuerCredentialSubject - } - issuer: { - display?: MetadataDisplay[] - id: string - } -} - -// what does this mean -const openId4VcCredentialMetadataKey = '_paradym/openId4VcCredentialMetadata' - -function extractOpenId4VcCredentialMetadata( - credentialMetadata: CredentialSupported, - serverMetadata: EndpointMetadata, - serverMetadataResult: CredentialIssuerMetadata | IssuerMetadataV1_0_08 -) { - return { - credential: { - display: credentialMetadata.display, - order: credentialMetadata.order, - credentialSubject: credentialMetadata.credentialSubject, - }, - issuer: { - display: serverMetadataResult.credentialIssuerMetadata?.display, - id: serverMetadata.issuer, - }, - } -} - -/** - * Gets the OpenId4Vc credential metadata from the given W3C credential record. - */ -export function getOpenId4VcCredentialMetadata( - w3cCredentialRecord: W3cCredentialRecord -): OpenId4VcCredentialMetadata | null { - return w3cCredentialRecord.metadata.get(openId4VcCredentialMetadataKey) -} - -/** - * Sets the OpenId4Vc credential metadata on the given W3C credential record. - * - * NOTE: this does not save the record. - */ -export function setOpenId4VcCredentialMetadata( - w3cCredentialRecord: W3cCredentialRecord, - credentialMetadata: CredentialSupported, - serverMetadata: EndpointMetadata, - serverMetadataResult: CredentialIssuerMetadata | IssuerMetadataV1_0_08 -) { - w3cCredentialRecord.metadata.set( - openId4VcCredentialMetadataKey, - extractOpenId4VcCredentialMetadata(credentialMetadata, serverMetadata, serverMetadataResult) - ) -} From f2648b51688ec63f2e84090e0bab2e8211eb7031 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 18 Oct 2023 12:31:12 +0200 Subject: [PATCH 014/115] feat: make metadata optional Signed-off-by: Martin Auer --- .../openid4vc-holder/src/OpenId4VcHolderService.ts | 11 ++++++++--- .../src/OpenId4VcHolderServiceOptions.ts | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderService.ts b/packages/openid4vc-holder/src/OpenId4VcHolderService.ts index 2e3802f2c4..8299509714 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderService.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderService.ts @@ -177,11 +177,12 @@ export class OpenId4VcHolderService { ) { let version = opts?.version ?? OpenId4VCIVersion.VER_1_0_11 const claimedCredentialOfferUrl = `openid-credential-offer://?` - const claimedIssuanceInitiaionUrl = `openid-initiate-issuance://?` + const claimedIssuanceInitiationUrl = `openid-initiate-issuance://?` if ( typeof credentialOffer === 'string' && - (credentialOffer.startsWith(claimedCredentialOfferUrl) || credentialOffer.startsWith(claimedIssuanceInitiaionUrl)) + (credentialOffer.startsWith(claimedCredentialOfferUrl) || + credentialOffer.startsWith(claimedIssuanceInitiationUrl)) ) { const credentialOfferWithBaseUrl = await CredentialOfferClient.fromURI(credentialOffer) credentialOffer = credentialOfferWithBaseUrl.credential_offer @@ -236,13 +237,17 @@ export class OpenId4VcHolderService { } public async acceptCredentialOffer(agentContext: AgentContext, options: RequestCredentialOptions) { - const { credentialsToRequest, credentialOfferPayload, metadata, version } = options + const { credentialsToRequest, credentialOfferPayload, metadata: _metadata, version } = options this.logger.info(`Accepting the following credential offers '${credentialsToRequest}'`) if (credentialsToRequest?.length === 0) { this.logger.warn(`Accepting 0 credential offers. Returning`) return [] } + const issuer = credentialOfferPayload.credential_issuer + const metadata = _metadata ? _metadata : MetadataClient.retrieveAllMetadata(issuer) + if (!metadata) throw new AriesFrameworkError(`Could not retrieve metadata for OpenID4VCI issuer: ${issuer}`) + const flowType = flowTypeMapping[options.flowType] if (!flowType) { throw new AriesFrameworkError( diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts index c6c05fd761..54c9213b64 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts @@ -197,7 +197,7 @@ type WithInternalOptions = Options & { * The endpoint metadata received from the credential issuer. * This is obtained manually or by calling the `resolveCredentialOffer` method. */ - metadata: EndpointMetadataResult + metadata?: EndpointMetadataResult /** * The resolved credential offer payload that was received from the issuer. From 791a06fbed1dce7e1d377cd001e58fdb26400900 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 18 Oct 2023 15:06:36 +0200 Subject: [PATCH 015/115] fix: tests Signed-off-by: Martin Auer --- .../src/OpenId4VcHolderService.ts | 2 +- .../src/OpenId4VcHolderServiceOptions.ts | 4 +- packages/openid4vc-holder/tests/fixtures.ts | 143 +++++++++++------- .../tests/openid4vc-holder.e2e.test.ts | 120 ++++++++------- 4 files changed, 152 insertions(+), 117 deletions(-) diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderService.ts b/packages/openid4vc-holder/src/OpenId4VcHolderService.ts index 8299509714..815cf4a76e 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderService.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderService.ts @@ -245,7 +245,7 @@ export class OpenId4VcHolderService { } const issuer = credentialOfferPayload.credential_issuer - const metadata = _metadata ? _metadata : MetadataClient.retrieveAllMetadata(issuer) + const metadata = _metadata ? _metadata : await MetadataClient.retrieveAllMetadata(issuer) if (!metadata) throw new AriesFrameworkError(`Could not retrieve metadata for OpenID4VCI issuer: ${issuer}`) const flowType = flowTypeMapping[options.flowType] diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts index 54c9213b64..7d5d914924 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts @@ -15,9 +15,7 @@ export const supportedCredentialFormats = [ ] satisfies OpenIdCredentialFormatProfile[] export type CredentialToRequest = { format: string; types: string[] } & ( - | { - offerType: OfferedCredentialType.InlineCredentialOffer - } + | { offerType: OfferedCredentialType.InlineCredentialOffer } | { offerType: OfferedCredentialType.CredentialSupported id: string | undefined diff --git a/packages/openid4vc-holder/tests/fixtures.ts b/packages/openid4vc-holder/tests/fixtures.ts index a71c9f2782..3b0ec92cf6 100644 --- a/packages/openid4vc-holder/tests/fixtures.ts +++ b/packages/openid4vc-holder/tests/fixtures.ts @@ -1,6 +1,92 @@ +const jsonLdPermanentResidentCardCredentialResponse = { + format: 'ldp_vc', + credential: { + type: ['VerifiableCredential', 'PermanentResidentCard'], + issuer: { + id: 'did:web:launchpad.vii.electron.mattrlabs.io', + name: 'Government of Kakapo', + logoUrl: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/logo.svg', + iconUrl: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/icon.svg', + image: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/icon.svg', + }, + name: 'Permanent Resident Card', + description: 'Government of Kakapo', + credentialBranding: { + backgroundColor: '#3a2d2d', + watermarkImageUrl: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/watermark@2x.png', + }, + issuanceDate: '2023-10-17T14:27:36.909Z', + credentialSubject: { + id: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', + type: ['PermanentResident', 'Person'], + image: + 'data:image/png;base64,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', + gender: 'Male', + birthDate: '1958-08-17', + givenName: 'Louis', + lprNumber: '1958-08-17', + familyName: 'Pasteur', + lprCategory: 'C09', + birthCountry: 'France', + residentSince: '2015-01-01', + commuterClassification: 'C1', + }, + '@context': [ + 'https://www.w3.org/2018/credentials/v1', + 'https://mattr.global/contexts/vc-extensions/v2', + 'https://w3id.org/citizenship/v1', + 'https://w3id.org/vc-revocation-list-2020/v1', + ], + credentialStatus: { + id: 'https://launchpad.vii.electron.mattrlabs.io/core/v1/revocation-lists/7bc7c021-56ee-445a-9143-fd79629df2aa#657', + type: 'RevocationList2020Status', + revocationListIndex: '657', + revocationListCredential: + 'https://launchpad.vii.electron.mattrlabs.io/core/v1/revocation-lists/7bc7c021-56ee-445a-9143-fd79629df2aa', + }, + proof: { + type: 'Ed25519Signature2018', + created: '2023-10-17T14:27:38Z', + jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..BaAHvbYzaFHp5rcqBChGVDd1gBpb9ezD4Rxn-Ev7uP1Jj71OfpcLH-oivuV90OGxgghaRwPe6rnBjwwo-RBjDg', + proofPurpose: 'assertionMethod', + verificationMethod: 'did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg', + }, + }, +} + export const mattrLaunchpadJsonLd_draft_08 = { + wellKnownDid: { + id: 'did:web:launchpad.vii.electron.mattrlabs.io', + '@context': [ + 'https://www.w3.org/ns/did/v1', + 'https://w3id.org/security/suites/x25519-2019/v1', + 'https://w3id.org/security/suites/ed25519-2018/v1', + ], + keyAgreement: [ + { + id: 'did:web:launchpad.vii.electron.mattrlabs.io#9eS8Tqsus1', + type: 'X25519KeyAgreementKey2019', + controller: 'did:web:launchpad.vii.electron.mattrlabs.io', + publicKeyBase58: '9eS8Tqsus1uJmQpf37S8CnEeBrEehsC3qz8RMq67KoLB', + }, + ], + authentication: ['did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg'], + assertionMethod: ['did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg'], + capabilityDelegation: ['did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg'], + capabilityInvocation: ['did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg'], + verificationMethod: [ + { + id: 'did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg', + type: 'Ed25519VerificationKey2018', + controller: 'did:web:launchpad.vii.electron.mattrlabs.io', + publicKeyBase58: '6BhFMCGTJg9DnpXZe7zbiTrtuwion5FVV6Z2NUpwDMVT', + }, + ], + }, credentialOffer: 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=OpenBadgeCredential&pre-authorized_code=krBcsBIlye2T-G4-rHHnRZUCah9uzDKwohJK6ABNvL-', + permanentResidentCardCredentialOffer: + 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=PermanentResidentCard&pre-authorized_code=krBcsBIlye2T-G4-rHHnRZUCah9uzDKwohJK6ABNvL-', getMetadataResponse: { authorization_endpoint: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/authorize', token_endpoint: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/token', @@ -134,6 +220,7 @@ export const mattrLaunchpadJsonLd_draft_08 = { }, }, }, + jsonLdCredentialResponse: jsonLdPermanentResidentCardCredentialResponse, } export const waltIdJffJwt_draft_08 = { @@ -458,59 +545,5 @@ export const waltIdJffJwt_draft_11 = { format: 'jwt_vc', }, - jsonLdCredentialResponse: { - format: 'ldp_vc', - credential: { - type: ['VerifiableCredential', 'PermanentResidentCard'], - issuer: { - id: 'did:web:launchpad.vii.electron.mattrlabs.io', - name: 'Government of Kakapo', - logoUrl: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/logo.svg', - iconUrl: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/icon.svg', - image: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/icon.svg', - }, - name: 'Permanent Resident Card', - description: 'Government of Kakapo', - credentialBranding: { - backgroundColor: '#3a2d2d', - watermarkImageUrl: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/watermark@2x.png', - }, - issuanceDate: '2023-10-17T14:27:36.909Z', - credentialSubject: { - id: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', - type: ['PermanentResident', 'Person'], - image: - 'data:image/png;base64,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', - gender: 'Male', - birthDate: '1958-08-17', - givenName: 'Louis', - lprNumber: '1958-08-17', - familyName: 'Pasteur', - lprCategory: 'C09', - birthCountry: 'France', - residentSince: '2015-01-01', - commuterClassification: 'C1', - }, - '@context': [ - 'https://www.w3.org/2018/credentials/v1', - 'https://mattr.global/contexts/vc-extensions/v2', - 'https://w3id.org/citizenship/v1', - 'https://w3id.org/vc-revocation-list-2020/v1', - ], - credentialStatus: { - id: 'https://launchpad.vii.electron.mattrlabs.io/core/v1/revocation-lists/7bc7c021-56ee-445a-9143-fd79629df2aa#657', - type: 'RevocationList2020Status', - revocationListIndex: '657', - revocationListCredential: - 'https://launchpad.vii.electron.mattrlabs.io/core/v1/revocation-lists/7bc7c021-56ee-445a-9143-fd79629df2aa', - }, - proof: { - type: 'Ed25519Signature2018', - created: '2023-10-17T14:27:38Z', - jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..BaAHvbYzaFHp5rcqBChGVDd1gBpb9ezD4Rxn-Ev7uP1Jj71OfpcLH-oivuV90OGxgghaRwPe6rnBjwwo-RBjDg', - proofPurpose: 'assertionMethod', - verificationMethod: 'did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg', - }, - }, - }, + jsonLdCredentialResponse: jsonLdPermanentResidentCardCredentialResponse, } diff --git a/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts index 46e1f842d7..2d3a964609 100644 --- a/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts @@ -61,20 +61,18 @@ describe('OpenId4VcHolder', () => { enableNetConnect() }) - xit('[DRAFT 08]: Should successfully execute the pre-authorized flow using a did:key Ed25519 subject and JSON-LD credential', async () => { + it('[DRAFT 08]: Should successfully execute the pre-authorized flow using a did:key Ed25519 subject and JSON-LD credential', async () => { const fixture = mattrLaunchpadJsonLd_draft_08 /** * Below we're setting up some mock HTTP responses. * These responses are based on the openid-initiate-issuance URI above * */ - // setup temporary redirect mock nock('https://launchpad.mattrlabs.com') .get('/.well-known/openid-credential-issuer') .reply(307, undefined, { Location: 'https://launchpad.vii.electron.mattrlabs.io/.well-known/openid-credential-issuer', }) - .get('/.well-known/openid-configuration') .reply(404) @@ -83,6 +81,10 @@ describe('OpenId4VcHolder', () => { // setup server metadata response nock('https://launchpad.vii.electron.mattrlabs.io') + .get('/.well-known/did.json') + .reply(200, fixture.wellKnownDid) + .get('/.well-known/did.json') + .reply(200, fixture.wellKnownDid) .get('/.well-known/openid-credential-issuer') .reply(200, fixture.getMetadataResponse) @@ -92,16 +94,12 @@ describe('OpenId4VcHolder', () => { // setup credential request response .post('/oidc/v1/auth/credential') - .reply(200, fixture.credentialResponse) + .reply(200, fixture.jsonLdCredentialResponse) const did = await agent.dids.create({ method: 'key', - options: { - keyType: KeyType.Ed25519, - }, - secret: { - privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), - }, + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, }) const didKey = DidKey.fromDid(did.didState.did as string) @@ -109,17 +107,20 @@ describe('OpenId4VcHolder', () => { const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) if (!verificationMethod) throw new Error('No verification method found') - const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer( - fixture.credentialOffer + const resolved = await agent.modules.openId4VcHolder.resolveCredentialOffer( + fixture.permanentResidentCardCredentialOffer ) const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( - resolvedCredentialOffer, + resolved, { verifyCredentialStatus: false, // We only allow EdDSa, as we've created a did with keyType ed25519. If we create // or determine the did dynamically we could use any signature algorithm allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + credentialsToRequest: resolved.credentialsToRequest.filter( + (c) => c.format === OpenIdCredentialFormatProfile.LdpVc + ), proofOfPossessionVerificationMethodResolver: () => verificationMethod, } ) @@ -128,11 +129,7 @@ describe('OpenId4VcHolder', () => { const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord expect(w3cCredentialRecord).toBeInstanceOf(W3cCredentialRecord) - expect(w3cCredentialRecord.credential.type).toEqual([ - 'VerifiableCredential', - 'VerifiableCredentialExtension', - 'OpenBadgeCredential', - ]) + expect(w3cCredentialRecord.credential.type).toEqual(['VerifiableCredential', 'PermanentResidentCard']) expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) }) @@ -306,9 +303,21 @@ describe('OpenId4VcHolder', () => { const fixture = mattrLaunchpadJsonLd_draft_08 // setup temporary redirect mock - nock('https://launchpad.mattrlabs.com').get('/.well-known/openid-credential-issuer').reply(307, undefined, { - Location: 'https://launchpad.vii.electron.mattrlabs.io/.well-known/openid-credential-issuer', - }) + nock('https://launchpad.mattrlabs.com') + .get('/.well-known/openid-credential-issuer') + .reply(307, undefined, { + Location: 'https://launchpad.vii.electron.mattrlabs.io/.well-known/openid-credential-issuer', + }) + .get('/.well-known/openid-configuration') + .reply(404) + .get('/.well-known/openid-configuration') + .reply(404) + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.getMetadataResponse) + .get('/.well-known/oauth-authorization-server') + .reply(404) + .get('/.well-known/oauth-authorization-server') + .reply(404) // setup server metadata response nock('https://launchpad.vii.electron.mattrlabs.io') @@ -329,12 +338,8 @@ describe('OpenId4VcHolder', () => { const did = await agent.dids.create({ method: 'key', - options: { - keyType: KeyType.Ed25519, - }, - secret: { - privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), - }, + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, }) const didKey = DidKey.fromDid(did.didState.did as string) @@ -346,7 +351,7 @@ describe('OpenId4VcHolder', () => { const redirectUri = 'https://example.com/cb' const initiationUri = - 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=OpenBadgeCredential' + 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=PermanentResidentCard' const scope = ['TestCredential'] const { codeVerifier } = await agent.modules.openId4VcHolder.generateAuthorizationUrl({ @@ -462,12 +467,11 @@ describe('OpenId4VcHolder', () => { const kid = `${didKey.did}#${didKey.key.fingerprint}` const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) if (!verificationMethod) throw new Error('No verification method found') - const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer( - fixture.credentialOffer - ) - expect(resolvedCredentialOffer.credentialsToRequest).toHaveLength(2) - const selectedCredentialsForRequest = resolvedCredentialOffer.credentialsToRequest.filter((credential) => { + const resolved = await agent.modules.openId4VcHolder.resolveCredentialOffer(fixture.credentialOffer) + expect(resolved.credentialsToRequest).toHaveLength(2) + + const selectedCredentialsForRequest = resolved.credentialsToRequest.filter((credential) => { return ( credential.format === OpenIdCredentialFormatProfile.JwtVcJson && credential.types.includes('VerifiableId') ) @@ -476,7 +480,7 @@ describe('OpenId4VcHolder', () => { expect(selectedCredentialsForRequest).toHaveLength(1) const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( - resolvedCredentialOffer, + resolved, { allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], proofOfPossessionVerificationMethodResolver: () => verificationMethod, @@ -613,29 +617,29 @@ describe('OpenId4VcHolder', () => { expect(w3cCredentialRecord1.credential.credentialSubjectIds[0]).toEqual(did.didState.did) }) - // it('use with jff / mattr demo', async () => { - // const did = await agent.dids.create({ - // method: 'key', - // options: { keyType: KeyType.Ed25519 }, - // secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, - // }) - - // const didKey = DidKey.fromDid(did.didState.did as string) - // const kid = `${didKey.did}#${didKey.key.fingerprint}` - // const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) - // if (!verificationMethod) throw new Error('No verification method found') - // const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer( - // `openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22PermanentResidentCard%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22t4y9cy-vBivTynmXuoPVT6cB8YIOo5mU-yKRUpx4vxB%22%7D%7D%7D` - // ) - - // const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( - // resolvedCredentialOffer, - // { - // allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - // proofOfPossessionVerificationMethodResolver: () => verificationMethod, - // verifyCredentialStatus: false, - // } - // ) - // }) + //it('use with jff / mattr demo', async () => { + // const did = await agent.dids.create({ + // method: 'key', + // options: { keyType: KeyType.Ed25519 }, + // secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, + // }) + + // const didKey = DidKey.fromDid(did.didState.did as string) + // const kid = `${didKey.did}#${didKey.key.fingerprint}` + // const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) + // if (!verificationMethod) throw new Error('No verification method found') + // const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer( + // `openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22PermanentResidentCard%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22q4ZLqvFxDVUA90IrjpemkjrkMWZV12efZ_YF6L0FE2T%22%7D%7D%7D` + // ) + + // const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + // resolvedCredentialOffer, + // { + // allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + // proofOfPossessionVerificationMethodResolver: () => verificationMethod, + // verifyCredentialStatus: false, + // } + // ) + //}) }) }) From fd87a827b481be6cc319f918db8d215a03c18fbe Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Thu, 19 Oct 2023 10:08:36 +0200 Subject: [PATCH 016/115] fix: test Signed-off-by: Martin Auer --- packages/openid4vc-holder/tests/fixtures.ts | 4 +- .../tests/openid4vc-holder.e2e.test.ts | 64 +++++++++---------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/packages/openid4vc-holder/tests/fixtures.ts b/packages/openid4vc-holder/tests/fixtures.ts index 3b0ec92cf6..d900c1d4c2 100644 --- a/packages/openid4vc-holder/tests/fixtures.ts +++ b/packages/openid4vc-holder/tests/fixtures.ts @@ -83,8 +83,8 @@ export const mattrLaunchpadJsonLd_draft_08 = { }, ], }, - credentialOffer: - 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=OpenBadgeCredential&pre-authorized_code=krBcsBIlye2T-G4-rHHnRZUCah9uzDKwohJK6ABNvL-', + credentialOfferAuthorizationCodeFlow: + 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=OpenBadgeCredential', permanentResidentCardCredentialOffer: 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=PermanentResidentCard&pre-authorized_code=krBcsBIlye2T-G4-rHHnRZUCah9uzDKwohJK6ABNvL-', getMetadataResponse: { diff --git a/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts index 2d3a964609..d99270bb83 100644 --- a/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts @@ -299,7 +299,7 @@ describe('OpenId4VcHolder', () => { }) // Need custom document loader for this - xit('[DRAFT 08]: should successfully execute request a credential', async () => { + it('[DRAFT 08]: should successfully execute request a credential', async () => { const fixture = mattrLaunchpadJsonLd_draft_08 // setup temporary redirect mock @@ -321,6 +321,10 @@ describe('OpenId4VcHolder', () => { // setup server metadata response nock('https://launchpad.vii.electron.mattrlabs.io') + .get('/.well-known/did.json') + .reply(200, fixture.wellKnownDid) + .get('/.well-known/did.json') + .reply(200, fixture.wellKnownDid) .get('/.well-known/openid-credential-issuer') .reply(200, fixture.getMetadataResponse) .get('/.well-known/openid-configuration') @@ -334,7 +338,7 @@ describe('OpenId4VcHolder', () => { // setup credential request response .post('/oidc/v1/auth/credential') - .reply(200, fixture.credentialResponse) + .reply(200, fixture.jsonLdCredentialResponse) const did = await agent.dids.create({ method: 'key', @@ -362,7 +366,7 @@ describe('OpenId4VcHolder', () => { }) const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer( - fixture.credentialOffer + fixture.credentialOfferAuthorizationCodeFlow ) const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( @@ -382,11 +386,7 @@ describe('OpenId4VcHolder', () => { const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord expect(w3cCredentialRecord).toBeInstanceOf(W3cCredentialRecord) - expect(w3cCredentialRecord.credential.type).toEqual([ - 'VerifiableCredential', - 'VerifiableCredentialExtension', - 'OpenBadgeCredential', - ]) + expect(w3cCredentialRecord.credential.type).toEqual(['VerifiableCredential', 'PermanentResidentCard']) expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) }) @@ -617,29 +617,29 @@ describe('OpenId4VcHolder', () => { expect(w3cCredentialRecord1.credential.credentialSubjectIds[0]).toEqual(did.didState.did) }) - //it('use with jff / mattr demo', async () => { - // const did = await agent.dids.create({ - // method: 'key', - // options: { keyType: KeyType.Ed25519 }, - // secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, - // }) - - // const didKey = DidKey.fromDid(did.didState.did as string) - // const kid = `${didKey.did}#${didKey.key.fingerprint}` - // const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) - // if (!verificationMethod) throw new Error('No verification method found') - // const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer( - // `openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22PermanentResidentCard%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22q4ZLqvFxDVUA90IrjpemkjrkMWZV12efZ_YF6L0FE2T%22%7D%7D%7D` - // ) - - // const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( - // resolvedCredentialOffer, - // { - // allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - // proofOfPossessionVerificationMethodResolver: () => verificationMethod, - // verifyCredentialStatus: false, - // } - // ) - //}) + // it('use with jff / mattr demo', async () => { + // const did = await agent.dids.create({ + // method: 'key', + // options: { keyType: KeyType.X25519 }, + // secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, + // }) + + // const credentialOffer = `openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22PermanentResidentCard%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22q4ZLqvFxDVUA90IrjpemkjrkMWZV12efZ_YF6L0FE2T%22%7D%7D%7D` + + // const didKey = DidKey.fromDid(did.didState.did as string) + // const kid = `${didKey.did}#${didKey.key.fingerprint}` + // const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) + // if (!verificationMethod) throw new Error('No verification method found') + // const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) + + // const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + // resolvedCredentialOffer, + // { + // allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], + // proofOfPossessionVerificationMethodResolver: () => verificationMethod, + // verifyCredentialStatus: false, + // } + // ) + // }) }) }) From aba50097ced5729019093c06e9f720aa2268a8e1 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 23 Oct 2023 11:12:07 +0200 Subject: [PATCH 017/115] feat: add changes from paradym wallet Signed-off-by: Martin Auer --- .../presentations/OpenId4VpHolderService.ts | 39 ++- .../PresentationExchangeService.ts | 239 +++++++++++++----- .../selection/PexCredentialSelection.ts | 75 ++++-- .../src/presentations/selection/types.ts | 17 +- .../src/presentations/transform.ts | 2 +- ....test.ts => openid4vci-holder.e2e.test.ts} | 54 ++-- .../tests/openid4vp-holder.e2e.test.ts | 83 ++++++ 7 files changed, 383 insertions(+), 126 deletions(-) rename packages/openid4vc-holder/tests/{openid4vc-holder.e2e.test.ts => openid4vci-holder.e2e.test.ts} (93%) create mode 100644 packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts diff --git a/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts b/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts index ef5628bb96..c1368a9021 100644 --- a/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts +++ b/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts @@ -1,4 +1,6 @@ -import type { AgentContext, W3cVerifiableCredential, W3cVerifiablePresentation } from '@aries-framework/core' +import type { PresentationSubmission } from './selection' +import type { CredentialsForInputDescriptor } from './selection/types' +import type { AgentContext, W3cCredentialRecord, W3cVerifiablePresentation } from '@aries-framework/core' import type { DIDDocument, PresentationDefinitionWithLocation, @@ -112,20 +114,40 @@ export class OpenId4VpHolderService { agentContext: AgentContext, options: { verifiedAuthorizationRequest: VerifiedAuthorizationRequestWithPresentationDefinition - selectedCredentials: W3cVerifiableCredential[] + submission: PresentationSubmission + submissionEntryIndexes: number[] } ) { const op = this.getOp(agentContext) - const vp = await this.presentationExchangeService.createPresentation(agentContext, { - selectedCredentials: options.selectedCredentials, + const credentialsForInputDescriptor: CredentialsForInputDescriptor = {} + + options.submission.requirements + .flatMap((requirement) => requirement.submission) + .forEach((submission, index) => { + const verifiableCredential = submission.verifiableCredentials[ + options.submissionEntryIndexes[index] as number + ] as W3cCredentialRecord + + const inputDescriptor = credentialsForInputDescriptor[submission.inputDescriptorId] + if (!inputDescriptor) { + credentialsForInputDescriptor[submission.inputDescriptorId] = [verifiableCredential.credential] + } else { + inputDescriptor.push(verifiableCredential.credential) + } + }) + + const vps = await this.presentationExchangeService.createPresentation(agentContext, { + credentialsForInputDescriptor, presentationDefinition: options.verifiedAuthorizationRequest.presentationDefinitions[0].definition, - // TODO: challenge / nonce + includePresentationSubmissionInVp: false, + // TODO: are there other properties we need to include? + nonce: await options.verifiedAuthorizationRequest.authorizationRequest.getMergedProperty('nonce'), }) const verificationMethod = await this.getVerificationMethodFromVerifiablePresentation( agentContext, - vp.verifiablePresentation + vps.verifiablePresentations[0] as W3cVerifiablePresentation ) const key = getKeyFromVerificationMethod(verificationMethod) const alg = getJwkClassFromKeyType(key.keyType)?.supportedSignatureAlgorithms[0] @@ -134,9 +156,10 @@ export class OpenId4VpHolderService { } const response = await op.createAuthorizationResponse(options.verifiedAuthorizationRequest, { + issuer: verificationMethod.controller, presentationExchange: { - verifiablePresentations: [vp.verifiablePresentation.encoded as W3CVerifiablePresentation], - presentationSubmission: vp.presentationSubmission, + verifiablePresentations: vps.verifiablePresentations.map((vp) => vp.encoded as W3CVerifiablePresentation), + presentationSubmission: vps.presentationSubmission, }, signature: { signature: async (data) => { diff --git a/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts b/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts index afa8af64a7..f3060b6515 100644 --- a/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts +++ b/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts @@ -1,4 +1,4 @@ -import type { PresentationSubmission } from './selection/types' +import type { CredentialsForInputDescriptor, PresentationSubmission } from './selection/types' import type { AgentContext, Query, @@ -6,8 +6,13 @@ import type { W3cCredentialRecord, W3cVerifiableCredential, } from '@aries-framework/core' -import type { PresentationSignCallBackParams } from '@sphereon/pex' -import type { PresentationDefinitionV1 } from '@sphereon/pex-models' +import type { PresentationSignCallBackParams, VerifiablePresentationResult } from '@sphereon/pex' +import type { + PresentationDefinitionV1, + PresentationDefinitionV2, + PresentationSubmission as PexPresentationSubmission, + Descriptor, +} from '@sphereon/pex-models' import type { IVerifiablePresentation } from '@sphereon/ssi-types' import { @@ -23,7 +28,7 @@ import { W3cPresentation, W3cCredentialRepository, } from '@aries-framework/core' -import { PEXv1, Status } from '@sphereon/pex' +import { PEVersion, PEX, Status } from '@sphereon/pex' import { selectCredentialsForRequest } from './selection/PexCredentialSelection' import { @@ -34,13 +39,13 @@ import { @injectable() export class PresentationExchangeService { - private pex = new PEXv1() + private pex = new PEX() /** * Validates a DIF Presentation Definition */ public validateDefinition(presentationDefinition: PresentationDefinitionV1) { - const result = PEXv1.validateDefinition(presentationDefinition) + const result = PEX.validateDefinition(presentationDefinition) // check if error const firstResult = Array.isArray(result) ? result[0] : result @@ -77,74 +82,148 @@ export class PresentationExchangeService { public async createPresentation( agentContext: AgentContext, { - selectedCredentials, + credentialsForInputDescriptor, presentationDefinition, challenge, domain, + nonce, + includePresentationSubmissionInVp = true, }: { - selectedCredentials: W3cVerifiableCredential[] - presentationDefinition: PresentationDefinitionV1 + credentialsForInputDescriptor: CredentialsForInputDescriptor + presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2 challenge?: string domain?: string + nonce?: string + includePresentationSubmissionInVp?: boolean } ) { - if (selectedCredentials.length === 0) { - throw new AriesFrameworkError('No credentials selected for creating presentation.') - } + // if (selectedCredentials.length === 0) { + // throw new AriesFrameworkError('No credentials selected for creating presentation.') + // } - // We use the subject id to resolve the DID document. - // I am assuming the subject is the same for all credentials (for now) - // The presentation contains multiple credentials and these are being added - // TODO how do we derive the verification method if there are multiple subject Ids - // FIXME - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const [firstSubjectId] = selectedCredentials[0]?.credentialSubjectIds ?? [] - - // Credential is allowed to be presented without a subject id. In that case we can't prove ownership of credential - // And it is more like a bearer token. - // In the future we can first check the holder key and if it exists we can use that as the one that should authenticate - // https://www.w3.org/TR/vc-data-model/#example-a-credential-issued-to-a-holder-who-is-not-the-only-subject-of-the-credential-who-has-no-relationship-with-the-subject-of-the-credential-but-who-has-a-relationship-with-the-issuer - if (!firstSubjectId) { - throw new AriesFrameworkError( - 'Credential subject missing from the selected credential for creating presentation.' + const vps: { + [subjectId: string]: { + [inputDescriptorId: string]: W3cVerifiableCredential[] + } + } = {} + + const verifiablePresentationResults: VerifiablePresentationResult[] = [] + + Object.entries(credentialsForInputDescriptor).forEach(([inputDescriptorId, credentials]) => { + credentials.forEach((credential) => { + const firstCredentialSubjectId = credential.credentialSubjectIds[0] + if (!firstCredentialSubjectId) { + throw new AriesFrameworkError( + 'Credential subject missing from the selected credential for creating presentation.' + ) + } + + const inputDescriptorsForSubject = vps[firstCredentialSubjectId] ?? {} + vps[firstCredentialSubjectId] = inputDescriptorsForSubject + + const credentialsForInputDescriptor = inputDescriptorsForSubject[inputDescriptorId] ?? [] + inputDescriptorsForSubject[inputDescriptorId] = credentialsForInputDescriptor + + credentialsForInputDescriptor.push(credential) + }) + }) + + for (const [subjectId, inputDescriptors] of Object.entries(vps)) { + // Determine a suitable verification method for the presentation + const verificationMethod = await this.getVerificationMethodForSubjectId(agentContext, subjectId) + + if (!verificationMethod) { + throw new AriesFrameworkError(`No verification method found for subject id ${subjectId}`) + } + + const inputDescriptorsForVp = (presentationDefinition.input_descriptors as PresentationDefinitionV1[]).filter( + (inputDescriptor) => inputDescriptor.id in inputDescriptors + ) + + const credentialsForVp = Object.values(inputDescriptors) + .flatMap((inputDescriptors) => inputDescriptors) + .map(getSphereonW3cVerifiableCredential) + + const presentationDefinitionForVp = { + ...presentationDefinition, + input_descriptors: inputDescriptorsForVp, + + // We remove the submission requirements, as it will otherwise fail to create the VP + // FIXME: Will this cause issue for creating the credential? Need to run tests + submission_requirements: undefined, + } + + // Q1: is holder always subject id, what if there are multiple subjects??? + // Q2: What about proofType, proofPurpose verification method for multiple subjects? + const verifiablePresentationResult = await this.pex.verifiablePresentationFrom( + presentationDefinitionForVp, + credentialsForVp, + this.getPresentationSignCallback( + agentContext, + verificationMethod, + // Can't include submission if more than one VP + Object.values(vps).length > 1 ? false : includePresentationSubmissionInVp + ), + { + holderDID: subjectId, + proofOptions: { + challenge, + domain, + nonce, + }, + signatureOptions: { + verificationMethod: verificationMethod?.id, + }, + } ) + + verifiablePresentationResults.push(verifiablePresentationResult) } - // Determine a suitable verification method for the presentation - const verificationMethod = await this.getVerificationMethodForSubjectId(agentContext, firstSubjectId) + const firstVerifiablePresentationResult = verifiablePresentationResults[0] + if (!firstVerifiablePresentationResult) { + throw new AriesFrameworkError('No verifiable presentations created.') + } - if (!verificationMethod) { - throw new AriesFrameworkError(`No verification method found for subject id ${firstSubjectId}`) + const presentationSubmission: PexPresentationSubmission = { + id: firstVerifiablePresentationResult.presentationSubmission.id, + definition_id: firstVerifiablePresentationResult.presentationSubmission.definition_id, + descriptor_map: [], } - // Q1: is holder always subject id, what if there are multiple subjects??? - // Q2: What about proofType, proofPurpose verification method for multiple subjects? - const verifiablePresentationResult = await this.pex.verifiablePresentationFrom( - presentationDefinition, - selectedCredentials.map(getSphereonW3cVerifiableCredential), - this.getPresentationSignCallback(agentContext, verificationMethod), - { - holderDID: firstSubjectId, - proofOptions: { - challenge, - domain, - // TODO: add nonce - }, - signatureOptions: { - verificationMethod: verificationMethod?.id, - }, - } - ) + for (const vp of verifiablePresentationResults) { + presentationSubmission.descriptor_map.push( + ...vp.presentationSubmission.descriptor_map.map((descriptor): Descriptor => { + const index = verifiablePresentationResults.indexOf(vp) + const prefix = verifiablePresentationResults.length > 1 ? `$[${index}]` : '$' + return { + format: 'jwt_vp', + path: prefix, + id: descriptor.id, + path_nested: { + ...descriptor, + path: descriptor.path.replace('$.', `${prefix}.vp.`), + format: 'jwt_vc_json', + }, + } + }) + ) + } return { - verifiablePresentation: getW3cVerifiablePresentationInstance(verifiablePresentationResult.verifiablePresentation), - presentationSubmission: verifiablePresentationResult.presentationSubmission, - presentationSubmissionLocation: verifiablePresentationResult.presentationSubmissionLocation, + verifiablePresentations: verifiablePresentationResults.map((r) => + getW3cVerifiablePresentationInstance(r.verifiablePresentation) + ), + presentationSubmission, + presentationSubmissionLocation: firstVerifiablePresentationResult.presentationSubmissionLocation, } } - public getPresentationSignCallback(agentContext: AgentContext, verificationMethod: VerificationMethod) { + public getPresentationSignCallback( + agentContext: AgentContext, + verificationMethod: VerificationMethod, + includePresentationSubmissionInVp = true + ) { const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) return async (callBackParams: PresentationSignCallBackParams) => { @@ -153,7 +232,14 @@ export class PresentationExchangeService { const { challenge, domain, nonce } = options.proofOptions ?? {} const { verificationMethod: verificationMethodId } = options.signatureOptions ?? {} - const w3cPresentation = JsonTransformer.fromJSON(presentationJson, W3cPresentation) + let presentationToSignJson = presentationJson + if (!includePresentationSubmissionInVp) { + presentationToSignJson = { + ...presentationToSignJson, + presentation_submission: undefined, + } + } + const w3cPresentation = JsonTransformer.fromJSON(presentationToSignJson, W3cPresentation) if (verificationMethodId && verificationMethodId !== verificationMethod.id) { throw new AriesFrameworkError( @@ -220,20 +306,47 @@ export class PresentationExchangeService { */ private async queryCredentialForPresentationDefinition( agentContext: AgentContext, - presentationDefinition: PresentationDefinitionV1 + presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2 ) { const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) const query: Array> = [] - // The schema.uri can contain either an expanded type, or a context uri - for (const inputDescriptor of presentationDefinition.input_descriptors) { - for (const schema of inputDescriptor.schema) { - // FIXME: It's currently not possible to query by the `type` of the credential. So we fetch all JWT VCs for now - query.push({ - $or: [{ expandedType: [schema.uri] }, { contexts: [schema.uri] }, { claimFormat: ClaimFormat.JwtVc }], - }) + const presentationDefinitionVersion = PEX.definitionVersionDiscovery(presentationDefinition) + + if (!presentationDefinitionVersion.version) { + throw new AriesFrameworkError( + `Unable to determine version for presentation definition. ${ + presentationDefinitionVersion.error ?? 'Unknown error' + }` + ) + } + + if (presentationDefinitionVersion.version === PEVersion.v1) { + const pd = presentationDefinition as PresentationDefinitionV1 + + // The schema.uri can contain either an expanded type, or a context uri + for (const inputDescriptor of pd.input_descriptors) { + for (const schema of inputDescriptor.schema) { + // FIXME: It's currently not possible to query by the `type` of the credential. So we fetch all JWT VCs for now + query.push({ + $or: [{ expandedType: [schema.uri] }, { contexts: [schema.uri] }, { claimFormat: ClaimFormat.JwtVc }], + }) + } } + } else if (presentationDefinitionVersion.version === PEVersion.v2) { + // FIXME: As PE version 2 does not have the `schema` anymore, we can't query by schema anymore. + // For now we retrieve ALL credentials, as we did the same for V1 with JWT credentials. We probably need + // to find some way to do initial filtering, hopefully if there's a filter on the `type` field or something. + + // FIXME: It's currently not possible to query by the `type` of the credential. So we fetch all JWT VCs for now + query.push({ + $or: [{ claimFormat: ClaimFormat.JwtVc }], + }) + } else { + throw new AriesFrameworkError( + `Unsupported presentation definition version ${presentationDefinitionVersion.version as unknown as string}` + ) } // query the wallet ourselves first to avoid the need to query the pex library for all diff --git a/packages/openid4vc-holder/src/presentations/selection/PexCredentialSelection.ts b/packages/openid4vc-holder/src/presentations/selection/PexCredentialSelection.ts index 630e6cfe06..702a4411c8 100644 --- a/packages/openid4vc-holder/src/presentations/selection/PexCredentialSelection.ts +++ b/packages/openid4vc-holder/src/presentations/selection/PexCredentialSelection.ts @@ -1,25 +1,45 @@ import type { PresentationSubmission, PresentationSubmissionRequirement, SubmissionEntry } from './types' import type { W3cCredentialRecord } from '@aries-framework/core' import type { SelectResults, SubmissionRequirementMatch } from '@sphereon/pex' -import type { PresentationDefinitionV1, SubmissionRequirement, InputDescriptorV1 } from '@sphereon/pex-models' -import type { OriginalVerifiableCredential } from '@sphereon/ssi-types' +import type { + PresentationDefinitionV1, + SubmissionRequirement, + InputDescriptorV1, + PresentationDefinitionV2, + InputDescriptorV2, +} from '@sphereon/pex-models' import { AriesFrameworkError } from '@aries-framework/core' -import { PEXv1 } from '@sphereon/pex' +import { PEX } from '@sphereon/pex' import { Rules } from '@sphereon/pex-models' import { default as jp } from 'jsonpath' import { getSphereonW3cVerifiableCredential } from '../transform' +/** + * Converts a camelCase string to a sentence format (first letter capitalized, rest in lower case). + * i.e. sanitizeString("helloWorld") // returns: 'Hello world' + */ +export function sanitizeString(str: string) { + const result = str.replace(/([a-z0-9])([A-Z])/g, '$1 $2') + let words = result.split(' ') + words = words.map((word, index) => { + if (index === 0) { + return word.charAt(0).toUpperCase() + word.slice(1) + } else { + return word.charAt(0).toLowerCase() + word.slice(1) + } + }) + return words.join(' ') +} + export function selectCredentialsForRequest( presentationDefinition: PresentationDefinitionV1, credentialRecords: W3cCredentialRecord[] ): PresentationSubmission { - const pex = new PEXv1() + const pex = new PEX() - const encodedCredentials: OriginalVerifiableCredential[] = credentialRecords.map((c) => - getSphereonW3cVerifiableCredential(c.credential) - ) + const encodedCredentials = credentialRecords.map((c) => getSphereonW3cVerifiableCredential(c.credential)) const selectResultsRaw = pex.selectFrom(presentationDefinition, encodedCredentials) @@ -130,7 +150,7 @@ function getSubmissionRequirements( } function getSubmissionRequirementsAllInputDescriptors( - presentationDefinition: PresentationDefinitionV1, + presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2, selectResults: W3cCredentialRecordSelectResults ): PresentationSubmissionRequirement[] { const submissionRequirements: PresentationSubmissionRequirement[] = [] @@ -139,7 +159,7 @@ function getSubmissionRequirementsAllInputDescriptors( const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) submissionRequirements.push({ - isRequirementSatisfied: submission.verifiableCredential !== undefined, + isRequirementSatisfied: submission.verifiableCredentials.length >= 1, submission: [submission], // Every input descriptor is a separate requirement, so the count is always 1 needsCount: 1, @@ -151,7 +171,7 @@ function getSubmissionRequirementsAllInputDescriptors( function getSubmissionRequirementRuleAll( submissionRequirement: SubmissionRequirement, - presentationDefinition: PresentationDefinitionV1, + presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2, selectResults: W3cCredentialRecordSelectResults ) { // Check if there's a 'from'. If not the structure is not as we expect it @@ -183,7 +203,7 @@ function getSubmissionRequirementRuleAll( // If all submissions have a credential, the requirement is satisfied isRequirementSatisfied: selectedSubmission.submission.every( - (submission) => submission.verifiableCredential !== undefined + (submission) => submission.verifiableCredentials.length >= 1 ), } } @@ -218,7 +238,7 @@ function getSubmissionRequirementRulePick( const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) - if (submission.verifiableCredential) { + if (submission.verifiableCredentials.length >= 1) { satisfiedSubmissions.push(submission) } else { unsatisfiedSubmissions.push(submission) @@ -246,37 +266,48 @@ function getSubmissionRequirementRulePick( } function getSubmissionForInputDescriptor( - inputDescriptor: InputDescriptorV1, + inputDescriptor: InputDescriptorV1 | InputDescriptorV2, selectResults: W3cCredentialRecordSelectResults ): SubmissionEntry { // https://github.com/Sphereon-Opensource/PEX/issues/116 // FIXME: the match.name is only the id if the input_descriptor has no name // Find first match - const match = selectResults.matches?.find( + const matches = selectResults.matches?.filter( (m) => m.name === inputDescriptor.id || // FIXME: this is not collision proof as the name doesn't have to be unique m.name === inputDescriptor.name ) + let name = inputDescriptor.name + // If there's no name on the input descriptor, but the id does not contain + // any special characters or numbers (so only letters and spaces), + // we will use a sanitized version of the id as the name + if (!name && inputDescriptor.id.match(/^[a-zA-Z ]+$/)) { + name = sanitizeString(inputDescriptor.id) + } + const submissionEntry: SubmissionEntry = { inputDescriptorId: inputDescriptor.id, - name: inputDescriptor.name, + name, purpose: inputDescriptor.purpose, + verifiableCredentials: [], } - // return early if no match. - if (!match) return submissionEntry + // return early if no matches. + if (!matches?.length) return submissionEntry // FIXME: This can return multiple credentials for multiple input_descriptors, // which I think is a bug in the PEX library // Extract all credentials from the match - const [verifiableCredential] = extractCredentialsFromMatch(match, selectResults.verifiableCredential) - - return { - ...submissionEntry, - verifiableCredential, + for (const match of matches) { + submissionEntry.verifiableCredentials = [ + ...submissionEntry.verifiableCredentials, + ...extractCredentialsFromMatch(match, selectResults.verifiableCredential), + ] } + + return submissionEntry } function extractCredentialsFromMatch(match: SubmissionRequirementMatch, availableCredentials?: W3cCredentialRecord[]) { diff --git a/packages/openid4vc-holder/src/presentations/selection/types.ts b/packages/openid4vc-holder/src/presentations/selection/types.ts index 72088c4ffa..b8feee36b5 100644 --- a/packages/openid4vc-holder/src/presentations/selection/types.ts +++ b/packages/openid4vc-holder/src/presentations/selection/types.ts @@ -1,4 +1,4 @@ -import type { W3cCredentialRecord } from '@aries-framework/core' +import type { W3cCredentialRecord, W3cVerifiableCredential } from '@aries-framework/core' /** * A submission entry that satisfies a specific input descriptor from the @@ -21,12 +21,12 @@ export interface SubmissionEntry { purpose?: string /** - * The verifiable credential that satisfies the input descriptor. + * The verifiable credentials that satisfy the input descriptor. * - * If the value is undefined, it means the input descriptor could + * If the value is an empty list, it means the input descriptor could * not be satisfied. */ - verifiableCredential?: W3cCredentialRecord + verifiableCredentials: W3cCredentialRecord[] } /** @@ -59,7 +59,7 @@ export interface PresentationSubmissionRequirement { * of the submission. * * NOTE: if the `isRequirementSatisfied` is `false` the submission list will - * contain entries without a verifiable credential. In this case it could also + * contain entries where the verifiable credential list is empty. In this case it could also * contain more entries than are actually needed (as you sometimes can choose from * e.g. 4 types of credentials and need to submit at least two). If * `isRequirementSatisfied` is `false`, make sure to check the `needsCount` value @@ -106,3 +106,10 @@ export interface PresentationSubmission { */ purpose?: string } + +/** + * Mapping of selected credentials for an input descriptor + */ +export interface CredentialsForInputDescriptor { + [inputDescriptorId: string]: W3cVerifiableCredential[] +} diff --git a/packages/openid4vc-holder/src/presentations/transform.ts b/packages/openid4vc-holder/src/presentations/transform.ts index 4576361d49..1cba1b91cc 100644 --- a/packages/openid4vc-holder/src/presentations/transform.ts +++ b/packages/openid4vc-holder/src/presentations/transform.ts @@ -1,6 +1,6 @@ import type { W3cVerifiableCredential, W3cVerifiablePresentation } from '@aries-framework/core' import type { - W3CVerifiableCredential as SphereonW3cVerifiableCredential, + OriginalVerifiableCredential as SphereonW3cVerifiableCredential, W3CVerifiablePresentation as SphereonW3cVerifiablePresentation, } from '@sphereon/ssi-types' diff --git a/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts similarity index 93% rename from packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts rename to packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts index d99270bb83..526ed1d4d3 100644 --- a/packages/openid4vc-holder/tests/openid4vc-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts @@ -37,10 +37,10 @@ describe('OpenId4VcHolder', () => { beforeEach(async () => { agent = new Agent({ config: { - label: 'OpenId4VcHolder Test10', + label: 'OpenId4VcHolder Test13', walletConfig: { - id: 'openid4vc-holder-test11', - key: 'openid4vc-holder-test12', + id: 'openid4vc-holder-test14', + key: 'openid4vc-holder-test15', }, }, dependencies: agentDependencies, @@ -617,29 +617,29 @@ describe('OpenId4VcHolder', () => { expect(w3cCredentialRecord1.credential.credentialSubjectIds[0]).toEqual(did.didState.did) }) - // it('use with jff / mattr demo', async () => { - // const did = await agent.dids.create({ - // method: 'key', - // options: { keyType: KeyType.X25519 }, - // secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, - // }) - - // const credentialOffer = `openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22PermanentResidentCard%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22q4ZLqvFxDVUA90IrjpemkjrkMWZV12efZ_YF6L0FE2T%22%7D%7D%7D` - - // const didKey = DidKey.fromDid(did.didState.did as string) - // const kid = `${didKey.did}#${didKey.key.fingerprint}` - // const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) - // if (!verificationMethod) throw new Error('No verification method found') - // const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) - - // const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( - // resolvedCredentialOffer, - // { - // allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], - // proofOfPossessionVerificationMethodResolver: () => verificationMethod, - // verifyCredentialStatus: false, - // } - // ) - // }) + it('use with jff / mattr demo', async () => { + const did = await agent.dids.create({ + method: 'key', + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, + }) + + const credentialOffer = `openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22PermanentResidentCard%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22a-Zxwinn9e3zuZQQHCDC502rdrwIGkA72J7FdPKhDQa%22%7D%7D%7D` + + const didKey = DidKey.fromDid(did.didState.did as string) + const kid = `${didKey.did}#${didKey.key.fingerprint}` + const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) + if (!verificationMethod) throw new Error('No verification method found') + const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) + + const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + resolvedCredentialOffer, + { + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + proofOfPossessionVerificationMethodResolver: () => verificationMethod, + verifyCredentialStatus: false, + } + ) + }) }) }) diff --git a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts new file mode 100644 index 0000000000..07a3bcbb45 --- /dev/null +++ b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts @@ -0,0 +1,83 @@ +import type { W3cCredentialRecord } from '@aries-framework/core' + +import { AskarModule } from '@aries-framework/askar' +import { KeyType, W3cJwtVerifiableCredential, Agent, Buffer } from '@aries-framework/core' +import { agentDependencies } from '@aries-framework/node' +import { ariesAskar } from '@hyperledger/aries-askar-nodejs' + +import { OpenId4VcHolderModule, OpenId4VpHolderService } from '../src' + +const modules = { + openId4VcClient: new OpenId4VcHolderModule(), + askar: new AskarModule({ + ariesAskar, + }), +} + +describe('OpenId4VcClient | OpenID4VP', () => { + let agent: Agent + + beforeEach(async () => { + agent = new Agent({ + config: { + label: 'OpenId4VcClient OpenID4VP Test', + walletConfig: { + id: 'openid4vc-client-openid4vp-test', + key: 'openid4vc-client-openid4vp-test', + }, + }, + dependencies: agentDependencies, + modules, + }) + + await agent.initialize() + }) + + afterEach(async () => { + await agent.shutdown() + await agent.wallet.delete() + }) + + describe('Mattr interop', () => { + // Not working yet. Once it works, we can mock the requests/responses + xit('Should succesfuly share a proof with MATTR launchpad', async () => { + // Store needed credential / did / key + await agent.w3cCredentials.storeCredential({ + credential: W3cJwtVerifiableCredential.fromSerializedJwt( + 'eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bGF1bmNocGFkLnZpaS5lbGVjdHJvbi5tYXR0cmxhYnMuaW8jNkJoRk1DR1RKZyJ9.eyJpc3MiOiJkaWQ6d2ViOmxhdW5jaHBhZC52aWkuZWxlY3Ryb24ubWF0dHJsYWJzLmlvIiwic3ViIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJuYmYiOjE2OTYwMjI5NDksImV4cCI6MTcyNzY0NTM0OSwidmMiOnsibmFtZSI6IkV4YW1wbGUgVW5pdmVyc2l0eSBEZWdyZWUiLCJkZXNjcmlwdGlvbiI6IkpGRiBQbHVnZmVzdCAzIE9wZW5CYWRnZSBDcmVkZW50aWFsIiwiY3JlZGVudGlhbEJyYW5kaW5nIjp7ImJhY2tncm91bmRDb2xvciI6IiM0NjRjNDkifSwiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL21hdHRyLmdsb2JhbC9jb250ZXh0cy92Yy1leHRlbnNpb25zL3YyIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjIuanNvbiIsImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9leHRlbnNpb25zLmpzb24iLCJodHRwczovL3czaWQub3JnL3ZjLXJldm9jYXRpb24tbGlzdC0yMDIwL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJPcGVuQmFkZ2VDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJ0eXBlIjpbIkFjaGlldmVtZW50U3ViamVjdCJdLCJhY2hpZXZlbWVudCI6eyJpZCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vYWNoaWV2ZW1lbnRzLzIxc3QtY2VudHVyeS1za2lsbHMvdGVhbXdvcmsiLCJuYW1lIjoiVGVhbXdvcmsiLCJ0eXBlIjpbIkFjaGlldmVtZW50Il0sImltYWdlIjp7ImlkIjoiaHR0cHM6Ly93M2MtY2NnLmdpdGh1Yi5pby92Yy1lZC9wbHVnZmVzdC0zLTIwMjMvaW1hZ2VzL0pGRi1WQy1FRFUtUExVR0ZFU1QzLWJhZGdlLWltYWdlLnBuZyIsInR5cGUiOiJJbWFnZSJ9LCJjcml0ZXJpYSI6eyJuYXJyYXRpdmUiOiJUZWFtIG1lbWJlcnMgYXJlIG5vbWluYXRlZCBmb3IgdGhpcyBiYWRnZSBieSB0aGVpciBwZWVycyBhbmQgcmVjb2duaXplZCB1cG9uIHJldmlldyBieSBFeGFtcGxlIENvcnAgbWFuYWdlbWVudC4ifSwiZGVzY3JpcHRpb24iOiJUaGlzIGJhZGdlIHJlY29nbml6ZXMgdGhlIGRldmVsb3BtZW50IG9mIHRoZSBjYXBhY2l0eSB0byBjb2xsYWJvcmF0ZSB3aXRoaW4gYSBncm91cCBlbnZpcm9ubWVudC4ifX0sImlzc3VlciI6eyJpZCI6ImRpZDp3ZWI6bGF1bmNocGFkLnZpaS5lbGVjdHJvbi5tYXR0cmxhYnMuaW8iLCJuYW1lIjoiRXhhbXBsZSBVbml2ZXJzaXR5IiwiaWNvblVybCI6Imh0dHBzOi8vdzNjLWNjZy5naXRodWIuaW8vdmMtZWQvcGx1Z2Zlc3QtMS0yMDIyL2ltYWdlcy9KRkZfTG9nb0xvY2t1cC5wbmciLCJpbWFnZSI6Imh0dHBzOi8vdzNjLWNjZy5naXRodWIuaW8vdmMtZWQvcGx1Z2Zlc3QtMS0yMDIyL2ltYWdlcy9KRkZfTG9nb0xvY2t1cC5wbmcifX19.HUYvivfEH2-yBXUq6t5gEZu1NY7_6tjsWojQvYbpRL_md5TyAmwn-LyfcPLyrQpgJcu08XjFp8smXFMfYJEqCQ' + ), + }) + + await agent.wallet.createKey({ + keyType: KeyType.Ed25519, + seed: Buffer.from('00000000000000000000000000000000'), + }) + + await agent.dids.import({ + did: 'did:key:z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + }) + + const openId4VpHolderService = agent.dependencyManager.resolve(OpenId4VpHolderService) + const { selectResults, verifiedAuthorizationRequest } = + await openId4VpHolderService.selectCredentialForProofRequest(agent.context, { + authorizationRequest: + 'openid4vp://authorize?client_id=https%3A%2F%2Flaunchpad.mattrlabs.com%2Fapi%2Fvp%2Fcallback&client_id_scheme=redirect_uri&response_uri=https%3A%2F%2Flaunchpad.mattrlabs.com%2Fapi%2Fvp%2Fcallback&response_type=vp_token&response_mode=direct_post&presentation_definition_uri=https%3A%2F%2Flaunchpad.mattrlabs.com%2Fapi%2Fvp%2Frequest%3Fstate%3D9b2nQuoLQkW0bX_vk24qjg&nonce=u-Wg1dR5wo5IqIr8ilshMQ&state=9b2nQuoLQkW0bX_vk24qjg', + }) + + if (!selectResults.areRequirementsSatisfied) { + throw new Error('Requirements are not satisfied.') + } + + const credentialRecords = selectResults.requirements + .flatMap((requirement) => requirement.submission.flatMap((submission) => submission.verifiableCredentials)) + .filter((credentialRecord): credentialRecord is W3cCredentialRecord => credentialRecord !== undefined) + + const credentials = credentialRecords.map((credentialRecord) => credentialRecord.credential) + + //await openId4VpHolderService.shareProof(agent.context, { + // verifiedAuthorizationRequest, + // selectedCredentials: credentials, + //}) + }) + }) +}) From 8aafb5d607738d82092caec8bf158fb63b054a9b Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 24 Oct 2023 11:04:47 +0200 Subject: [PATCH 018/115] feat: authorization code flow (alpha) Signed-off-by: Martin Auer --- packages/openid4vc-holder/package.json | 4 +- .../src/OpenId4VcHolderApi.ts | 5 - .../src/OpenId4VcHolderModule.ts | 4 +- .../src/OpenId4VcHolderService.ts | 225 +++++++++++++----- .../src/OpenId4VcHolderServiceOptions.ts | 26 +- packages/openid4vc-holder/src/index.ts | 1 - .../tests/openid4vci-holder.e2e.test.ts | 160 +++++-------- yarn.lock | 19 +- 8 files changed, 259 insertions(+), 185 deletions(-) diff --git a/packages/openid4vc-holder/package.json b/packages/openid4vc-holder/package.json index 7a340e6d44..3dd359d460 100644 --- a/packages/openid4vc-holder/package.json +++ b/packages/openid4vc-holder/package.json @@ -25,8 +25,8 @@ }, "dependencies": { "@aries-framework/core": "0.4.2", - "@sphereon/oid4vci-client": "^0.7.3", - "@sphereon/oid4vci-common": "^0.7.3", + "@sphereon/oid4vci-client": "^0.8.1", + "@sphereon/oid4vci-common": "^0.8.1", "@sphereon/did-auth-siop": "^0.4.2", "@sphereon/pex": "^2.1.3-unstable.6", "@sphereon/pex-models": "^2.1.1", diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts index 7f48fdc2e6..0c0d2a0436 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts @@ -1,5 +1,4 @@ import type { - GenerateAuthorizationUrlOptions, PreAuthCodeFlowOptions, AuthCodeFlowOptions, ResolvedCredentialOffer, @@ -59,8 +58,4 @@ export class OpenId4VcHolderApi { flowType: AuthFlowType.AuthorizationCodeFlow, }) } - - public async generateAuthorizationUrl(options: GenerateAuthorizationUrlOptions) { - return this.openId4VcHolderService.generateAuthorizationUrl(options) - } } diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts b/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts index b45615624f..0fbedaf240 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts @@ -8,7 +8,8 @@ import { PresentationExchangeService } from './presentations' import { OpenId4VpHolderService } from './presentations/OpenId4VpHolderService' /** - * @public + * @public @module OpenId4VcHolderModule + * This module provides the functionality to assume the role of owner in relation to the OpenId4VC specification suite. */ export class OpenId4VcHolderModule implements Module { public readonly api = OpenId4VcHolderApi @@ -27,7 +28,6 @@ export class OpenId4VcHolderModule implements Module { // Api dependencyManager.registerContextScoped(OpenId4VcHolderApi) - // Services // Services dependencyManager.registerSingleton(OpenId4VcHolderService) dependencyManager.registerSingleton(OpenId4VpHolderService) diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderService.ts b/packages/openid4vc-holder/src/OpenId4VcHolderService.ts index 815cf4a76e..b182695e0b 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderService.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderService.ts @@ -1,6 +1,6 @@ import type { + AuthDetails, CredentialToRequest, - GenerateAuthorizationUrlOptions, ProofOfPossessionRequirements, ProofOfPossessionVerificationMethodResolver, RequestCredentialOptions, @@ -15,6 +15,7 @@ import type { W3cVerifyCredentialResult, } from '@aries-framework/core' import type { + AccessTokenResponse, CredentialOfferFormat, CredentialOfferPayloadV1_0_11, CredentialResponse, @@ -23,6 +24,7 @@ import type { Jwt, OpenIDResponse, ProofOfPossessionCallbacks, + PushedAuthorizationResponse, UniformCredentialOfferPayload, } from '@sphereon/oid4vci-common' @@ -38,7 +40,6 @@ import { SignatureSuiteRegistry, TypedArrayEncoder, W3cCredentialRecord, - W3cCredentialRepository, W3cCredentialService, W3cJsonLdVerifiableCredential, W3cJwtVerifiableCredential, @@ -51,19 +52,24 @@ import { injectable, parseDid, } from '@aries-framework/core' +import { Metadata } from '@aries-framework/core/src/storage/Metadata' import { AccessTokenClient, CredentialOfferClient, CredentialRequestClientBuilder, MetadataClient, - OpenID4VCIClient, ProofOfPossessionBuilder, + formPost, + getJson, } from '@sphereon/oid4vci-client' import { OpenId4VCIVersion, AuthzFlowType, CodeChallengeMethod, assertedUniformCredentialOffer, + ResponseType, + convertJsonToURI, + JsonURIMode, } from '@sphereon/oid4vci-common' import { randomStringForEntropy } from '@stablelib/random' @@ -91,6 +97,21 @@ const flowTypeMapping = { [AuthFlowType.PreAuthorizedCodeFlow]: AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW, } +interface AcquireAuthorizationCodeResult { + code: string +} + +interface AuthRequestOpts { + credentialOffer: CredentialOfferPayloadV1_0_11 + metadata: EndpointMetadataResult + clientId: string + codeChallenge: string + codeChallengeMethod: CodeChallengeMethod + authorizationDetails?: AuthDetails | AuthDetails[] + redirectUri: string + scope?: string +} + /** * @internal */ @@ -98,62 +119,129 @@ const flowTypeMapping = { export class OpenId4VcHolderService { private logger: Logger private w3cCredentialService: W3cCredentialService - private w3cCredentialRepository: W3cCredentialRepository private jwsService: JwsService public constructor( @inject(InjectionSymbols.Logger) logger: Logger, w3cCredentialService: W3cCredentialService, - w3cCredentialRepository: W3cCredentialRepository, jwsService: JwsService ) { this.w3cCredentialService = w3cCredentialService - this.w3cCredentialRepository = w3cCredentialRepository this.jwsService = jwsService this.logger = logger } - private generateCodeVerifier(): string { - return randomStringForEntropy(256) + // TODO: copied from sphereon + private handleAuthorizationDetails( + metadata: EndpointMetadataResult, + authorizationDetails?: AuthDetails | AuthDetails[] + ): AuthDetails | AuthDetails[] | undefined { + if (authorizationDetails) { + if (Array.isArray(authorizationDetails)) { + return authorizationDetails.map((value) => this.handleLocations({ ...value }, metadata)) + } else { + return this.handleLocations({ ...authorizationDetails }, metadata) + } + } + return authorizationDetails + } + + // TODO copied from sphereon + private handleLocations(authorizationDetails: AuthDetails, metadata: EndpointMetadataResult) { + if ( + authorizationDetails && + (metadata.credentialIssuerMetadata?.authorization_server || metadata.authorization_endpoint) + ) { + if (authorizationDetails.locations) { + if (Array.isArray(authorizationDetails.locations)) { + ;(authorizationDetails.locations as string[]).push(metadata.issuer) + } else { + authorizationDetails.locations = [authorizationDetails.locations as string, metadata.issuer] + } + } else { + authorizationDetails.locations = metadata.issuer + } + } + return authorizationDetails } - public async generateAuthorizationUrl(options: GenerateAuthorizationUrlOptions) { - this.logger.debug('Generating authorization url') + // TODO: copied from sphereon + public async acquireAuthorizationRequestCode({ + credentialOffer, + metadata, + clientId, + codeChallengeMethod, + codeChallenge, + authorizationDetails, + redirectUri, + scope, + }: AuthRequestOpts): Promise { + // Scope and authorization_details can be used in the same authorization request + // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param + if (!scope && !authorizationDetails) { + throw new AriesFrameworkError('Please provide a scope or authorization_details') + } - if (!options.scope || options.scope.length === 0) { + // Authorization servers supporting PAR SHOULD include the URL of their pushed authorization request endpoint in their authorization server metadata document + // Note that the presence of pushed_authorization_request_endpoint is sufficient for a client to determine that it may use the PAR flow. + // What happens if it doesn't ??? + let parEndpoint = metadata.credentialIssuerMetadata?.pushed_authorization_request_endpoint + if (typeof parEndpoint !== 'string') parEndpoint = undefined + + let authorizationEndpoint = metadata.credentialIssuerMetadata?.authorization_endpoint + if (typeof parEndpoint !== 'string') authorizationEndpoint = undefined + + if (!parEndpoint && !authorizationEndpoint) { throw new AriesFrameworkError( - 'Only scoped based authorization requests are supported at this time. Please provide at least one scope' + "Server metadata does not contain 'pushed_authorization_request_endpoint' or 'authorization_endpoint'. Which are required for the Authorization Code Flow" ) } - // TODO: how should people get this URI - const client = await OpenID4VCIClient.fromURI({ - uri: options.initiationUri, - flowType: AuthzFlowType.AUTHORIZATION_CODE_FLOW, - }) + // add 'openid' scope if not present + if (!scope?.includes('openid')) { + scope = ['openid', scope].filter((s) => !!s).join(' ') + } - const codeVerifier = this.generateCodeVerifier() - const codeVerifierSha256 = Hasher.hash(TypedArrayEncoder.fromString(codeVerifier), 'sha2-256') - const base64Url = TypedArrayEncoder.toBase64URL(codeVerifierSha256) + const queryObj: { [key: string]: string } = { + response_type: ResponseType.AUTH_CODE, + client_id: clientId, + code_challenge_method: codeChallengeMethod, + code_challenge: codeChallenge, + authorization_details: JSON.stringify(this.handleAuthorizationDetails(metadata, authorizationDetails)), + redirect_uri: redirectUri, + scope: scope, + } - this.logger.debug('Converted code_verifier to code_challenge', { - codeVerifier: codeVerifier, - sha256: codeVerifierSha256.toString(), - base64Url: base64Url, - }) + const issuerState = credentialOffer.grants?.authorization_code?.issuer_state + if (issuerState) queryObj['issuer_state'] = issuerState + + let requestUri: string + + if (parEndpoint) { + const response = await formPost(parEndpoint, new URLSearchParams(queryObj)) + if (!response.successBody) + throw new AriesFrameworkError(`Could not acquire the authorization request uri from '${parEndpoint}'`) + requestUri = convertJsonToURI( + { request_uri: response.successBody.request_uri }, + { + baseUrl: metadata.credentialIssuerMetadata?.authorization_endpoint, + uriTypeProperties: ['request_uri'], + mode: JsonURIMode.X_FORM_WWW_URLENCODED, + } + ) + } else { + requestUri = convertJsonToURI(queryObj, { + baseUrl: authorizationEndpoint, + uriTypeProperties: ['redirect_uri', 'scope', 'authorization_details', 'issuer_state'], + mode: JsonURIMode.X_FORM_WWW_URLENCODED, + }) + } - const authorizationUrl = client.createAuthorizationRequestUrl({ - clientId: options.clientId, - codeChallengeMethod: CodeChallengeMethod.SHA256, - codeChallenge: base64Url, - redirectUri: options.redirectUri, - scope: options.scope?.join(' '), - }) + const response = await getJson(requestUri) + if (!response.successBody) + throw new AriesFrameworkError(`Could not acquire the authorization request code from '${parEndpoint}'`) - return { - authorizationUrl, - codeVerifier, - } + return { code: response.successBody.code } } private getFormatAndTypesFromOfferedCredential( @@ -238,12 +326,14 @@ export class OpenId4VcHolderService { public async acceptCredentialOffer(agentContext: AgentContext, options: RequestCredentialOptions) { const { credentialsToRequest, credentialOfferPayload, metadata: _metadata, version } = options - this.logger.info(`Accepting the following credential offers '${credentialsToRequest}'`) + if (credentialsToRequest?.length === 0) { this.logger.warn(`Accepting 0 credential offers. Returning`) return [] } + this.logger.info(`Accepting the following credential offers '${credentialsToRequest}'`) + const issuer = credentialOfferPayload.credential_issuer const metadata = _metadata ? _metadata : await MetadataClient.retrieveAllMetadata(issuer) if (!metadata) throw new AriesFrameworkError(`Could not retrieve metadata for OpenID4VCI issuer: ${issuer}`) @@ -268,29 +358,46 @@ export class OpenId4VcHolderService { } // acquire the access token - // NOTE: only scope based flow is supported for authorized flow. However there's not clear mapping between - // the scope property and which credential to request (this is out of scope of the spec), so it will still - // just request all credentials that have been offered in the credential offer. We may need to add some extra - // input properties that allows to define the credential type(s) to request. + let openIdAccessTokenResponse: OpenIDResponse + const accessTokenClient = new AccessTokenClient() - const openIdAccessTokenResponse = - options.flowType === AuthFlowType.AuthorizationCodeFlow - ? await accessTokenClient.acquireAccessToken({ - metadata, - credentialOffer: { - credential_offer: credentialOfferPayload, - }, - code: options.authorizationCode, - codeVerifier: options.codeVerifier, - redirectUri: options.redirectUri, - }) - : await accessTokenClient.acquireAccessToken({ - metadata, - credentialOffer: { - credential_offer: credentialOfferPayload, - }, - pin: options.userPin, - }) + if (options.flowType === AuthFlowType.AuthorizationCodeFlow) { + const codeVerifier = randomStringForEntropy(256) + const codeVerifierSha256 = Hasher.hash(TypedArrayEncoder.fromString(codeVerifier), 'sha2-256') + const base64Url = TypedArrayEncoder.toBase64URL(codeVerifierSha256) + + this.logger.debug('Converted code_verifier to code_challenge', { + codeVerifier: codeVerifier, + sha256: codeVerifierSha256.toString(), + base64Url: base64Url, + }) + + const result = await this.acquireAuthorizationRequestCode({ + credentialOffer: credentialOfferPayload, + clientId: options.clientId, + codeChallengeMethod: CodeChallengeMethod.SHA256, + codeChallenge: base64Url, + redirectUri: options.redirectUri, + scope: options.scope && options.scope.length === 0 ? undefined : options.scope?.join(' '), + authorizationDetails: options.authDetails && options.authDetails.length === 0 ? undefined : options.authDetails, + metadata, + }) + + openIdAccessTokenResponse = await accessTokenClient.acquireAccessToken({ + metadata, + credentialOffer: { credential_offer: credentialOfferPayload }, + code: result.code, + codeVerifier: codeVerifier, + redirectUri: options.redirectUri, + pin: options.userPin, + }) + } else { + openIdAccessTokenResponse = await accessTokenClient.acquireAccessToken({ + metadata, + credentialOffer: { credential_offer: credentialOfferPayload }, + pin: options.userPin, + }) + } if (!openIdAccessTokenResponse.successBody) { throw new AriesFrameworkError(`could not acquire access token from '${metadata.issuer}'`) diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts index 7d5d914924..491b6aa150 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts @@ -1,9 +1,19 @@ import type { OfferedCredentialType } from './OpenId4VcHolderService' import type { JwaSignatureAlgorithm, KeyType, VerificationMethod } from '@aries-framework/core' import type { CredentialOfferPayloadV1_0_11, EndpointMetadataResult, OpenId4VCIVersion } from '@sphereon/oid4vci-common' +import type { CredentialFormat } from '@sphereon/ssi-types' import { OpenIdCredentialFormatProfile } from './utils/claimFormatMapping' +// TODO: use simpler object +export interface AuthDetails { + type: 'openid_credential' | string + locations?: string | string[] + format: CredentialFormat | CredentialFormat[] + + [s: string]: unknown +} + /** * The credential formats that are supported by the openid4vc holder */ @@ -76,24 +86,10 @@ export interface PreAuthCodeFlowOptions { * Extends the pre-authorized code flow options. */ export interface AuthCodeFlowOptions extends PreAuthCodeFlowOptions { - clientId: string - authorizationCode: string - codeVerifier: string - redirectUri: string -} - -/** - * The options that are used to generate the authorization url. - * - * NOTE: The `code_challenge` property is omitted here - * because we assume it will always be SHA256 - * as clear text code challenges are unsafe. - */ -export interface GenerateAuthorizationUrlOptions { - initiationUri: string clientId: string redirectUri: string scope?: string[] + authDetails?: AuthDetails[] } export interface ProofOfPossessionVerificationMethodResolverOptions { diff --git a/packages/openid4vc-holder/src/index.ts b/packages/openid4vc-holder/src/index.ts index e1fea1adac..83719b5004 100644 --- a/packages/openid4vc-holder/src/index.ts +++ b/packages/openid4vc-holder/src/index.ts @@ -6,7 +6,6 @@ export * from './OpenId4VcHolderService' // Contains internal types, so we don't export everything export { AuthCodeFlowOptions, - GenerateAuthorizationUrlOptions, PreAuthCodeFlowOptions, ProofOfPossessionVerificationMethodResolver, ProofOfPossessionVerificationMethodResolverOptions, diff --git a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts index 526ed1d4d3..bbf8d6e6f2 100644 --- a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts @@ -61,7 +61,7 @@ describe('OpenId4VcHolder', () => { enableNetConnect() }) - it('[DRAFT 08]: Should successfully execute the pre-authorized flow using a did:key Ed25519 subject and JSON-LD credential', async () => { + xit('[DRAFT 08]: Should successfully execute the pre-authorized flow using a did:key Ed25519 subject and JSON-LD credential', async () => { const fixture = mattrLaunchpadJsonLd_draft_08 /** * Below we're setting up some mock HTTP responses. @@ -204,7 +204,7 @@ describe('OpenId4VcHolder', () => { enableNetConnect() }) - it('[DRAFT 08]: should generate a valid authorization url', async () => { + it('[DRAFT 08]: should throw if no scope and no authorization_details are provided', async () => { const fixture = mattrLaunchpadJsonLd_draft_08 // setup temporary redirect mock @@ -215,55 +215,21 @@ describe('OpenId4VcHolder', () => { }) .get('/.well-known/openid-configuration') .reply(404) - .get('/.well-known/oauth-authorization-server') + .get('/.well-known/openid-configuration') .reply(404) - - // setup server metadata response - nock('https://launchpad.vii.electron.mattrlabs.io') .get('/.well-known/openid-credential-issuer') .reply(200, fixture.getMetadataResponse) - - // setup access token response - .post('/oidc/v1/auth/token') - .reply(200, fixture.acquireAccessTokenResponse) - - // setup credential request response - .post('/oidc/v1/auth/credential') - .reply(200, fixture.credentialResponse) - - const clientId = 'test-client' - - const redirectUri = 'https://example.com/cb' - const scope = ['TestCredential'] - const initiationUri = - 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=OpenBadgeCredential' - const { authorizationUrl } = await agent.modules.openId4VcHolder.generateAuthorizationUrl({ - clientId, - redirectUri, - scope, - initiationUri, - }) - - const parsedUrl = new URL(authorizationUrl) - expect(authorizationUrl.startsWith('https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/authorize')).toBe( - true - ) - expect(parsedUrl.searchParams.get('response_type')).toBe('code') - expect(parsedUrl.searchParams.get('client_id')).toBe(clientId) - expect(parsedUrl.searchParams.get('code_challenge_method')).toBe('S256') - expect(parsedUrl.searchParams.get('redirect_uri')).toBe(redirectUri) - }) - - it('[DRAFT 08]: should throw if no scope is provided', async () => { - const fixture = mattrLaunchpadJsonLd_draft_08 - - // setup temporary redirect mock - nock('https://launchpad.mattrlabs.com').get('/.well-known/openid-credential-issuer').reply(307, undefined, { - Location: 'https://launchpad.vii.electron.mattrlabs.io/.well-known/openid-credential-issuer', - }) + .get('/.well-known/oauth-authorization-server') + .reply(404) + .get('/.well-known/oauth-authorization-server') + .reply(404) // setup server metadata response nock('https://launchpad.vii.electron.mattrlabs.io') + .get('/.well-known/did.json') + .reply(200, fixture.wellKnownDid) + .get('/.well-known/did.json') + .reply(200, fixture.wellKnownDid) .get('/.well-known/openid-credential-issuer') .reply(200, fixture.getMetadataResponse) .get('/.well-known/openid-configuration') @@ -277,29 +243,39 @@ describe('OpenId4VcHolder', () => { // setup credential request response .post('/oidc/v1/auth/credential') - .reply(200, fixture.credentialResponse) + .reply(200, fixture.jsonLdCredentialResponse) - // setup server metadata response - nock('https://launchpad.vii.electron.mattrlabs.io') - .get('/.well-known/openid-credential-issuer') - .reply(200, fixture.getMetadataResponse) + const did = await agent.dids.create({ + method: 'key', + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, + }) + + const didKey = DidKey.fromDid(did.didState.did as string) + const kid = `${did.didState.did as string}#${didKey.key.fingerprint}` + const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) + if (!verificationMethod) throw new Error('No verification method found') const clientId = 'test-client' const redirectUri = 'https://example.com/cb' - const initiationUri = - 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=OpenBadgeCredential' + + const resolved = await agent.modules.openId4VcHolder.resolveCredentialOffer( + fixture.credentialOfferAuthorizationCodeFlow + ) + await expect( - agent.modules.openId4VcHolder.generateAuthorizationUrl({ - clientId, - redirectUri, - scope: [], - initiationUri, + agent.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode(resolved, { + clientId: clientId, + verifyCredentialStatus: false, + proofOfPossessionVerificationMethodResolver: () => verificationMethod, + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + redirectUri: redirectUri, }) ).rejects.toThrow() }) // Need custom document loader for this - it('[DRAFT 08]: should successfully execute request a credential', async () => { + xit('[DRAFT 08]: should successfully execute request a credential', async () => { const fixture = mattrLaunchpadJsonLd_draft_08 // setup temporary redirect mock @@ -352,18 +328,8 @@ describe('OpenId4VcHolder', () => { if (!verificationMethod) throw new Error('No verification method found') const clientId = 'test-client' - const redirectUri = 'https://example.com/cb' - const initiationUri = - 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=PermanentResidentCard' - const scope = ['TestCredential'] - const { codeVerifier } = await agent.modules.openId4VcHolder.generateAuthorizationUrl({ - clientId, - redirectUri, - scope, - initiationUri, - }) const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer( fixture.credentialOfferAuthorizationCodeFlow @@ -373,12 +339,11 @@ describe('OpenId4VcHolder', () => { resolvedCredentialOffer, { clientId: clientId, - authorizationCode: 'test-code', - codeVerifier: codeVerifier, verifyCredentialStatus: false, proofOfPossessionVerificationMethodResolver: () => verificationMethod, allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], redirectUri: redirectUri, + scope, } ) @@ -502,7 +467,7 @@ describe('OpenId4VcHolder', () => { expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) }) - it('[DRAFT 11]: Should successfully execute the pre-authorized flow using a single offered credential a did:key EdDSA subject and JsonLd format', async () => { + xit('[DRAFT 11]: Should successfully execute the pre-authorized flow using a single offered credential a did:key EdDSA subject and JsonLd format', async () => { const fixture = waltIdJffJwt_draft_11 const httpMock = nock('https://jff.walt.id/issuer-api/default/oidc') .get('/.well-known/openid-credential-issuer') @@ -559,7 +524,7 @@ describe('OpenId4VcHolder', () => { expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) }) - it('[DRAFT 11]: Should successfully execute the pre-authorized for multiple credentials of different formats using a did:key EdDsa subject', async () => { + xit('[DRAFT 11]: Should successfully execute the pre-authorized for multiple credentials of different formats using a did:key EdDsa subject', async () => { const fixture = waltIdJffJwt_draft_11 const httpMock = nock('https://jff.walt.id/issuer-api/default/oidc') .get('/.well-known/openid-credential-issuer') @@ -617,29 +582,32 @@ describe('OpenId4VcHolder', () => { expect(w3cCredentialRecord1.credential.credentialSubjectIds[0]).toEqual(did.didState.did) }) - it('use with jff / mattr demo', async () => { - const did = await agent.dids.create({ - method: 'key', - options: { keyType: KeyType.Ed25519 }, - secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, - }) - - const credentialOffer = `openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22ldp_vc%22%2C%22types%22%3A%5B%22PermanentResidentCard%22%5D%7D%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22a-Zxwinn9e3zuZQQHCDC502rdrwIGkA72J7FdPKhDQa%22%7D%7D%7D` - - const didKey = DidKey.fromDid(did.didState.did as string) - const kid = `${didKey.did}#${didKey.key.fingerprint}` - const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) - if (!verificationMethod) throw new Error('No verification method found') - const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) - - const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( - resolvedCredentialOffer, - { - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - proofOfPossessionVerificationMethodResolver: () => verificationMethod, - verifyCredentialStatus: false, - } - ) - }) + //it('use with jff / mattr demo', async () => { + // const did = await agent.dids.create({ + // method: 'key', + // options: { keyType: KeyType.Ed25519 }, + // secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, + // }) + + // const credentialOffer = `openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.portal.walt.id%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22VerifiableAttestation%22%2C%22VerifiableDiploma%22%5D%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fw3id.org%2Fsecurity%2Fsuites%2Fjws-2020%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22VerifiableAttestation%22%2C%22VerifiableDiploma%22%5D%7D%7D%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22dd8c7950-3f4e-4c61-8b40-4be18c980e46%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJkZDhjNzk1MC0zZjRlLTRjNjEtOGI0MC00YmUxOGM5ODBlNDYiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.UB8riE2_SNxRE_0jXStlpwDrkusmNnCQZgBAGW74xmi3BKPQgnqIB4m_MTHKjA9KhVitKjCoWH8iJdD7nQDVDQ%22%2C%22user_pin_required%22%3Afalse%7D%7D%7D` + + // const didKey = DidKey.fromDid(did.didState.did as string) + // const kid = `${didKey.did}#${didKey.key.fingerprint}` + // const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) + // if (!verificationMethod) throw new Error('No verification method found') + // const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) + + // const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( + // resolvedCredentialOffer, + // { + // allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + // proofOfPossessionVerificationMethodResolver: () => verificationMethod, + // verifyCredentialStatus: false, + // clientId: 'https://issuer.portal.walt.id', + // redirectUri: 'https://example.com/cb', + // scope: ['openid'], + // } + // ) + //}) }) }) diff --git a/yarn.lock b/yarn.lock index 8a39dd29d6..fd6a486bef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2438,12 +2438,12 @@ cross-fetch "^3.1.5" did-resolver "^4.1.0" -"@sphereon/oid4vci-client@^0.7.3": - version "0.7.3" - resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-client/-/oid4vci-client-0.7.3.tgz#48d7ef9ec4d3ab64944f2c2bb1de9b8b5e90cfbb" - integrity sha512-9xQvpLGYqDtqjcK2R1KfCKNBJUEqhLsA5lJrxV40DQ6fTddz7lVJWommX+pRqKDRR+N6tAo80qgqzELQEzJw2w== +"@sphereon/oid4vci-client@^0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-client/-/oid4vci-client-0.8.1.tgz#e05ce5d0f9d5227492b7e5264864cea88a4e10a4" + integrity sha512-NhIxBDTvXRDl7du+z3Mnpm0VkxI1L/r1r4hJzz2Xdh1NKruzk5lkoLbRyq9uCc3rMg/RH8U+nkThrOE0TiwnRg== dependencies: - "@sphereon/oid4vci-common" "0.7.3" + "@sphereon/oid4vci-common" "0.8.1" "@sphereon/ssi-types" "0.17.2" cross-fetch "^3.1.8" debug "^4.3.4" @@ -2457,6 +2457,15 @@ cross-fetch "^3.1.8" jwt-decode "^3.1.2" +"@sphereon/oid4vci-common@0.8.1", "@sphereon/oid4vci-common@^0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-common/-/oid4vci-common-0.8.1.tgz#2623f467c3765a96f3330691d6a0946f04360106" + integrity sha512-lKdVjUIkd04Tpt9BLZ+N5mv2uhm4x1Qu5TFrYHBPzXCc0jNMnSWCN3yD0HjfdsW++unLaqAqifvM4iYY5NKSZg== + dependencies: + "@sphereon/ssi-types" "0.17.2" + cross-fetch "^3.1.8" + jwt-decode "^3.1.2" + "@sphereon/oid4vci-issuer@^0.7.3": version "0.7.3" resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-issuer/-/oid4vci-issuer-0.7.3.tgz#862c34b9e4c3c3c95485971cd58471729e4b20db" From 18f6d72b2b8a16dc321ffc69cf396369d7c0cd95 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 25 Oct 2023 16:33:09 +0200 Subject: [PATCH 019/115] feat: authorization code flow Signed-off-by: Martin Auer --- .../src/OpenId4VcHolderApi.ts | 68 +++++-- .../src/OpenId4VcHolderService.ts | 180 ++++++++++-------- .../src/OpenId4VcHolderServiceOptions.ts | 21 +- packages/openid4vc-holder/src/index.ts | 2 +- 4 files changed, 165 insertions(+), 106 deletions(-) diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts index 0c0d2a0436..65da9faddd 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts @@ -1,15 +1,17 @@ import type { - PreAuthCodeFlowOptions, - AuthCodeFlowOptions, ResolvedCredentialOffer, + AuthCodeFlowOptions, + AcceptCredentialOfferOptions, + ResolvedAuthorizationRequest, } from './OpenId4VcHolderServiceOptions' -import type { W3cCredentialRecord } from '@aries-framework/core' +import type { VerificationMethod, W3cCredentialRecord } from '@aries-framework/core' +import type { VerifiedAuthorizationRequest } from '@sphereon/did-auth-siop' import type { CredentialOfferPayloadV1_0_11 } from '@sphereon/oid4vci-common' import { injectable, AgentContext } from '@aries-framework/core' import { OpenId4VcHolderService } from './OpenId4VcHolderService' -import { AuthFlowType } from './OpenId4VcHolderServiceOptions' +import { OpenId4VpHolderService } from './presentations/OpenId4VpHolderService' /** * @public @@ -18,10 +20,30 @@ import { AuthFlowType } from './OpenId4VcHolderServiceOptions' export class OpenId4VcHolderApi { private agentContext: AgentContext private openId4VcHolderService: OpenId4VcHolderService + private openId4VpHolderService: OpenId4VpHolderService - public constructor(agentContext: AgentContext, openId4VcHolderService: OpenId4VcHolderService) { + public constructor( + agentContext: AgentContext, + openId4VcHolderService: OpenId4VcHolderService, + openId4VpHolderService: OpenId4VpHolderService + ) { this.agentContext = agentContext this.openId4VcHolderService = openId4VcHolderService + this.openId4VpHolderService = openId4VpHolderService + } + + public async resolveRequest(uri: string) { + const resolved = await this.openId4VpHolderService.resolveAuthenticationRequest(this.agentContext, uri) + return resolved + } + + public async acceptRequest(verifiedRequest: VerifiedAuthorizationRequest, verificationMethod: VerificationMethod) { + const resolved = await this.openId4VpHolderService.acceptRequest( + this.agentContext, + verifiedRequest, + verificationMethod + ) + return resolved } public async resolveCredentialOffer(credentialOffer: string | CredentialOfferPayloadV1_0_11) { @@ -29,33 +51,37 @@ export class OpenId4VcHolderApi { return resolved } + public async resolveAuthorizationRequest( + resolvedCredentialOffer: ResolvedCredentialOffer, + authCodeFlowOptions: AuthCodeFlowOptions + ) { + const uri = await this.openId4VcHolderService.resolveAuthorizationRequest( + resolvedCredentialOffer, + authCodeFlowOptions + ) + return uri + } + public async acceptCredentialOfferUsingPreAuthorizedCode( resolvedCredentialOffer: ResolvedCredentialOffer, - options: PreAuthCodeFlowOptions + options: AcceptCredentialOfferOptions ): Promise { - // set defaults - const verifyRevocationState = options.verifyCredentialStatus ?? true - return this.openId4VcHolderService.acceptCredentialOffer(this.agentContext, { - ...resolvedCredentialOffer, - ...options, - verifyCredentialStatus: verifyRevocationState, - flowType: AuthFlowType.PreAuthorizedCodeFlow, + resolvedCredentialOffer, + acceptCredentialOfferOptions: options, }) } public async acceptCredentialOfferUsingAuthorizationCode( resolvedCredentialOffer: ResolvedCredentialOffer, - options: AuthCodeFlowOptions + resolvedAuthorizationRequest: ResolvedAuthorizationRequest, + code: string, + acceptCredentialOfferOptions: AcceptCredentialOfferOptions ): Promise { - // set defaults - const checkRevocationState = options.verifyCredentialStatus ?? true - return this.openId4VcHolderService.acceptCredentialOffer(this.agentContext, { - ...resolvedCredentialOffer, - ...options, - verifyCredentialStatus: checkRevocationState, - flowType: AuthFlowType.AuthorizationCodeFlow, + resolvedCredentialOffer, + resolvedAuthorizationRequest: { ...resolvedAuthorizationRequest, code }, + acceptCredentialOfferOptions, }) } } diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderService.ts b/packages/openid4vc-holder/src/OpenId4VcHolderService.ts index b182695e0b..b6c5cde09b 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderService.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderService.ts @@ -1,10 +1,13 @@ import type { + AuthCodeFlowOptions, AuthDetails, CredentialToRequest, + AcceptCredentialOfferOptions, ProofOfPossessionRequirements, ProofOfPossessionVerificationMethodResolver, - RequestCredentialOptions, SupportedCredentialFormats, + ResolvedCredentialOffer, + ResolvedAuthorizationRequest, } from './OpenId4VcHolderServiceOptions' import type { OpenIdCredentialFormatProfile } from './utils' import type { @@ -52,7 +55,6 @@ import { injectable, parseDid, } from '@aries-framework/core' -import { Metadata } from '@aries-framework/core/src/storage/Metadata' import { AccessTokenClient, CredentialOfferClient, @@ -60,11 +62,9 @@ import { MetadataClient, ProofOfPossessionBuilder, formPost, - getJson, } from '@sphereon/oid4vci-client' import { OpenId4VCIVersion, - AuthzFlowType, CodeChallengeMethod, assertedUniformCredentialOffer, ResponseType, @@ -73,7 +73,7 @@ import { } from '@sphereon/oid4vci-common' import { randomStringForEntropy } from '@stablelib/random' -import { AuthFlowType, supportedCredentialFormats } from './OpenId4VcHolderServiceOptions' +import { supportedCredentialFormats } from './OpenId4VcHolderServiceOptions' import { fromOpenIdCredentialFormatProfileToDifClaimFormat } from './utils' import { getUniformFormat } from './utils/Formats' import { getSupportedCredentials } from './utils/IssuerMetadataUtils' @@ -92,11 +92,6 @@ export type OfferedCredentialsWithMetadata = | { credentialSupported: CredentialSupported; type: OfferedCredentialType.CredentialSupported } | { inlineCredentialOffer: CredentialOfferFormat; type: OfferedCredentialType.InlineCredentialOffer } -const flowTypeMapping = { - [AuthFlowType.AuthorizationCodeFlow]: AuthzFlowType.AUTHORIZATION_CODE_FLOW, - [AuthFlowType.PreAuthorizedCodeFlow]: AuthzFlowType.PRE_AUTHORIZED_CODE_FLOW, -} - interface AcquireAuthorizationCodeResult { code: string } @@ -107,9 +102,9 @@ interface AuthRequestOpts { clientId: string codeChallenge: string codeChallengeMethod: CodeChallengeMethod - authorizationDetails?: AuthDetails | AuthDetails[] + authDetails?: AuthDetails | AuthDetails[] redirectUri: string - scope?: string + scope?: string[] } /** @@ -172,13 +167,16 @@ export class OpenId4VcHolderService { clientId, codeChallengeMethod, codeChallenge, - authorizationDetails, redirectUri, - scope, - }: AuthRequestOpts): Promise { + scope: _scope, + authDetails: _authDetails, + }: AuthRequestOpts) { + let scope = !_scope || _scope.length === 0 ? undefined : _scope?.join(' ') + const authDetails = !_authDetails || _authDetails.length === 0 ? undefined : _authDetails + // Scope and authorization_details can be used in the same authorization request // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param - if (!scope && !authorizationDetails) { + if (!scope && !authDetails) { throw new AriesFrameworkError('Please provide a scope or authorization_details') } @@ -189,11 +187,11 @@ export class OpenId4VcHolderService { if (typeof parEndpoint !== 'string') parEndpoint = undefined let authorizationEndpoint = metadata.credentialIssuerMetadata?.authorization_endpoint - if (typeof parEndpoint !== 'string') authorizationEndpoint = undefined + if (typeof authorizationEndpoint !== 'string') authorizationEndpoint = undefined - if (!parEndpoint && !authorizationEndpoint) { + if (!authorizationEndpoint) { throw new AriesFrameworkError( - "Server metadata does not contain 'pushed_authorization_request_endpoint' or 'authorization_endpoint'. Which are required for the Authorization Code Flow" + "Server metadata does not contain 'authorization_endpoint'. Which is required for the Authorization Code Flow" ) } @@ -203,45 +201,41 @@ export class OpenId4VcHolderService { } const queryObj: { [key: string]: string } = { - response_type: ResponseType.AUTH_CODE, client_id: clientId, + response_type: ResponseType.AUTH_CODE, code_challenge_method: codeChallengeMethod, code_challenge: codeChallenge, - authorization_details: JSON.stringify(this.handleAuthorizationDetails(metadata, authorizationDetails)), redirect_uri: redirectUri, - scope: scope, } + if (scope) queryObj['scope'] = scope + + const authorizationDetails = JSON.stringify(this.handleAuthorizationDetails(metadata, authDetails)) + if (authorizationDetails) queryObj['authorization_details'] = authorizationDetails + const issuerState = credentialOffer.grants?.authorization_code?.issuer_state if (issuerState) queryObj['issuer_state'] = issuerState - let requestUri: string - if (parEndpoint) { - const response = await formPost(parEndpoint, new URLSearchParams(queryObj)) + const body = new URLSearchParams(queryObj) + const response = await formPost(parEndpoint, body) if (!response.successBody) throw new AriesFrameworkError(`Could not acquire the authorization request uri from '${parEndpoint}'`) - requestUri = convertJsonToURI( - { request_uri: response.successBody.request_uri }, + return convertJsonToURI( + { request_uri: response.successBody.request_uri, client_id: clientId, response_type: ResponseType.AUTH_CODE }, { - baseUrl: metadata.credentialIssuerMetadata?.authorization_endpoint, - uriTypeProperties: ['request_uri'], + baseUrl: authorizationEndpoint, + uriTypeProperties: ['request_uri', 'client_id', 'response_type'], mode: JsonURIMode.X_FORM_WWW_URLENCODED, } ) } else { - requestUri = convertJsonToURI(queryObj, { + return convertJsonToURI(queryObj, { baseUrl: authorizationEndpoint, uriTypeProperties: ['redirect_uri', 'scope', 'authorization_details', 'issuer_state'], mode: JsonURIMode.X_FORM_WWW_URLENCODED, }) } - - const response = await getJson(requestUri) - if (!response.successBody) - throw new AriesFrameworkError(`Could not acquire the authorization request code from '${parEndpoint}'`) - - return { code: response.successBody.code } } private getFormatAndTypesFromOfferedCredential( @@ -324,8 +318,65 @@ export class OpenId4VcHolderService { } } - public async acceptCredentialOffer(agentContext: AgentContext, options: RequestCredentialOptions) { - const { credentialsToRequest, credentialOfferPayload, metadata: _metadata, version } = options + public async resolveAuthorizationRequest( + resolvedCredentialOffer: ResolvedCredentialOffer, + authCodeFlowOptions: AuthCodeFlowOptions + ): Promise { + const { credentialOfferPayload, metadata: _metadata } = resolvedCredentialOffer + + // TODO: authdetails + + const issuer = credentialOfferPayload.credential_issuer + const metadata = _metadata ? _metadata : await MetadataClient.retrieveAllMetadata(issuer) + if (!metadata) throw new AriesFrameworkError(`Could not retrieve metadata for OpenID4VCI issuer: ${issuer}`) + + const codeVerifier = randomStringForEntropy(256) + const codeVerifierSha256 = Hasher.hash(TypedArrayEncoder.fromString(codeVerifier), 'sha2-256') + const codeChallenge = TypedArrayEncoder.toBase64URL(codeVerifierSha256) + + this.logger.debug('Converted code_verifier to code_challenge', { + codeVerifier: codeVerifier, + sha256: codeVerifierSha256.toString(), + base64Url: codeChallenge, + }) + + const { clientId, redirectUri, scope, authDetails } = authCodeFlowOptions + const authorizationRequestUri = await this.acquireAuthorizationRequestCode({ + credentialOffer: credentialOfferPayload, + clientId, + codeChallengeMethod: CodeChallengeMethod.SHA256, + codeChallenge, + redirectUri, + scope, + authDetails, + metadata, + }) + + return { + ...authCodeFlowOptions, + codeVerifier, + authorizationRequestUri, + } + } + + public async acceptCredentialOffer( + agentContext: AgentContext, + options: { + resolvedCredentialOffer: ResolvedCredentialOffer + acceptCredentialOfferOptions: AcceptCredentialOfferOptions + resolvedAuthorizationRequest?: ResolvedAuthorizationRequest & { code: string } + } + ) { + const { resolvedCredentialOffer, acceptCredentialOfferOptions, resolvedAuthorizationRequest } = options + + const { + credentialsToRequest, + allowedProofOfPossessionSignatureAlgorithms: _allowedProofOfPossessionSignatureAlgorithms, + userPin, + proofOfPossessionVerificationMethodResolver, + verifyCredentialStatus, + } = acceptCredentialOfferOptions + const { credentialOfferPayload, metadata: _metadata, version } = resolvedCredentialOffer if (credentialsToRequest?.length === 0) { this.logger.warn(`Accepting 0 credential offers. Returning`) @@ -338,17 +389,13 @@ export class OpenId4VcHolderService { const metadata = _metadata ? _metadata : await MetadataClient.retrieveAllMetadata(issuer) if (!metadata) throw new AriesFrameworkError(`Could not retrieve metadata for OpenID4VCI issuer: ${issuer}`) - const flowType = flowTypeMapping[options.flowType] - if (!flowType) { - throw new AriesFrameworkError( - `Unsupported flowType ${options.flowType}. Valid values are ${Object.values(AuthFlowType).join(', ')}` - ) - } + const issuerMetadata = metadata.credentialIssuerMetadata + if (!issuerMetadata) throw new AriesFrameworkError('Found no credential issuer metadata') const supportedJwaSignatureAlgorithms = this.getSupportedJwaSignatureAlgorithms(agentContext) - const allowedProofOfPossessionSignatureAlgorithms = options.allowedProofOfPossessionSignatureAlgorithms - ? options.allowedProofOfPossessionSignatureAlgorithms.filter((algorithm) => + const allowedProofOfPossessionSignatureAlgorithms = _allowedProofOfPossessionSignatureAlgorithms + ? _allowedProofOfPossessionSignatureAlgorithms.filter((algorithm) => supportedJwaSignatureAlgorithms.includes(algorithm) ) : supportedJwaSignatureAlgorithms @@ -361,41 +408,21 @@ export class OpenId4VcHolderService { let openIdAccessTokenResponse: OpenIDResponse const accessTokenClient = new AccessTokenClient() - if (options.flowType === AuthFlowType.AuthorizationCodeFlow) { - const codeVerifier = randomStringForEntropy(256) - const codeVerifierSha256 = Hasher.hash(TypedArrayEncoder.fromString(codeVerifier), 'sha2-256') - const base64Url = TypedArrayEncoder.toBase64URL(codeVerifierSha256) - - this.logger.debug('Converted code_verifier to code_challenge', { - codeVerifier: codeVerifier, - sha256: codeVerifierSha256.toString(), - base64Url: base64Url, - }) - - const result = await this.acquireAuthorizationRequestCode({ - credentialOffer: credentialOfferPayload, - clientId: options.clientId, - codeChallengeMethod: CodeChallengeMethod.SHA256, - codeChallenge: base64Url, - redirectUri: options.redirectUri, - scope: options.scope && options.scope.length === 0 ? undefined : options.scope?.join(' '), - authorizationDetails: options.authDetails && options.authDetails.length === 0 ? undefined : options.authDetails, - metadata, - }) - + if (resolvedAuthorizationRequest) { + const { code, codeVerifier, redirectUri } = resolvedAuthorizationRequest openIdAccessTokenResponse = await accessTokenClient.acquireAccessToken({ metadata, credentialOffer: { credential_offer: credentialOfferPayload }, - code: result.code, - codeVerifier: codeVerifier, - redirectUri: options.redirectUri, - pin: options.userPin, + code, + codeVerifier, + redirectUri, + pin: userPin, }) } else { openIdAccessTokenResponse = await accessTokenClient.acquireAccessToken({ metadata, credentialOffer: { credential_offer: credentialOfferPayload }, - pin: options.userPin, + pin: userPin, }) } @@ -406,9 +433,6 @@ export class OpenId4VcHolderService { const accessToken = openIdAccessTokenResponse.successBody - const issuerMetadata = metadata.credentialIssuerMetadata - if (!issuerMetadata) throw new AriesFrameworkError('Found no credential issuer metadata') - const offeredCredentialsWithMetadata = this.getOfferedCredentialsWithMetadata( credentialOfferPayload, issuerMetadata, @@ -438,7 +462,7 @@ export class OpenId4VcHolderService { allowedCredentialFormats: supportedCredentialFormats, allowedProofOfPossessionSignatureAlgorithms, offeredCredentialWithMetadata: credentialWithMetadata, - proofOfPossessionVerificationMethodResolver: options.proofOfPossessionVerificationMethodResolver, + proofOfPossessionVerificationMethodResolver: proofOfPossessionVerificationMethodResolver, }) const callbacks: ProofOfPossessionCallbacks = { @@ -494,7 +518,7 @@ export class OpenId4VcHolderService { }) const credential = await this.handleCredentialResponse(agentContext, credentialResponse, { - verifyCredentialStatus: options.verifyCredentialStatus, + verifyCredentialStatus, }) // Create credential record, but we don't store it yet (only after the user has accepted the credential) diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts index 491b6aa150..9b4d352d92 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts @@ -41,10 +41,15 @@ export interface ResolvedCredentialOffer { credentialsToRequest: CredentialToRequest[] } +export interface ResolvedAuthorizationRequest extends AuthCodeFlowOptions { + codeVerifier: string + authorizationRequestUri: string +} + /** - * Options that are used for the pre-authorized code flow. + * Options that are used to accept a credential offer for both the pre-authorized code flow and authorization code flow. */ -export interface PreAuthCodeFlowOptions { +export interface AcceptCredentialOfferOptions { /** * String value containing a user PIN. This value MUST be present if user_pin_required was set to true in the Credential Offer. * This parameter MUST only be used, if the grant_type is urn:ietf:params:oauth:grant-type:pre-authorized_code. @@ -85,7 +90,7 @@ export interface PreAuthCodeFlowOptions { * Options that are used for the authorization code flow. * Extends the pre-authorized code flow options. */ -export interface AuthCodeFlowOptions extends PreAuthCodeFlowOptions { +export interface AuthCodeFlowOptions { clientId: string redirectUri: string scope?: string[] @@ -206,10 +211,14 @@ type WithInternalOptions = Options & { version: OpenId4VCIVersion } +export type AuthorizationCodeFlowOptions = WithInternalOptions +export type PreAuthorizedCodeFlowOptions = WithInternalOptions< + AuthFlowType.PreAuthorizedCodeFlow, + AcceptCredentialOfferOptions +> + /** * The options that are used to request a credential from an issuer. * @internal */ -export type RequestCredentialOptions = - | WithInternalOptions - | WithInternalOptions +export type RequestCredentialOptions = PreAuthorizedCodeFlowOptions & AuthorizationCodeFlowOptions diff --git a/packages/openid4vc-holder/src/index.ts b/packages/openid4vc-holder/src/index.ts index 83719b5004..97dd2a45fb 100644 --- a/packages/openid4vc-holder/src/index.ts +++ b/packages/openid4vc-holder/src/index.ts @@ -6,7 +6,7 @@ export * from './OpenId4VcHolderService' // Contains internal types, so we don't export everything export { AuthCodeFlowOptions, - PreAuthCodeFlowOptions, + AcceptCredentialOfferOptions, ProofOfPossessionVerificationMethodResolver, ProofOfPossessionVerificationMethodResolverOptions, RequestCredentialOptions, From 60369efb98380512121c4939d87377fda022a4f5 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 25 Oct 2023 16:35:33 +0200 Subject: [PATCH 020/115] refactor: rename Signed-off-by: Martin Auer --- .../src/{OpenId4VcHolderApi.ts => OpenId4VciHolderApi.ts} | 4 ++-- .../{OpenId4VcHolderModule.ts => OpenId4VciHolderModule.ts} | 4 ++-- .../{OpenId4VcHolderService.ts => OpenId4VciHolderService.ts} | 4 ++-- ...derServiceOptions.ts => OpenId4VciHolderServiceOptions.ts} | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) rename packages/openid4vc-holder/src/{OpenId4VcHolderApi.ts => OpenId4VciHolderApi.ts} (96%) rename packages/openid4vc-holder/src/{OpenId4VcHolderModule.ts => OpenId4VciHolderModule.ts} (91%) rename packages/openid4vc-holder/src/{OpenId4VcHolderService.ts => OpenId4VciHolderService.ts} (99%) rename packages/openid4vc-holder/src/{OpenId4VcHolderServiceOptions.ts => OpenId4VciHolderServiceOptions.ts} (99%) diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts b/packages/openid4vc-holder/src/OpenId4VciHolderApi.ts similarity index 96% rename from packages/openid4vc-holder/src/OpenId4VcHolderApi.ts rename to packages/openid4vc-holder/src/OpenId4VciHolderApi.ts index 65da9faddd..707fd97d6b 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts +++ b/packages/openid4vc-holder/src/OpenId4VciHolderApi.ts @@ -3,14 +3,14 @@ import type { AuthCodeFlowOptions, AcceptCredentialOfferOptions, ResolvedAuthorizationRequest, -} from './OpenId4VcHolderServiceOptions' +} from './OpenId4VciHolderServiceOptions' import type { VerificationMethod, W3cCredentialRecord } from '@aries-framework/core' import type { VerifiedAuthorizationRequest } from '@sphereon/did-auth-siop' import type { CredentialOfferPayloadV1_0_11 } from '@sphereon/oid4vci-common' import { injectable, AgentContext } from '@aries-framework/core' -import { OpenId4VcHolderService } from './OpenId4VcHolderService' +import { OpenId4VcHolderService } from './OpenId4VciHolderService' import { OpenId4VpHolderService } from './presentations/OpenId4VpHolderService' /** diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts b/packages/openid4vc-holder/src/OpenId4VciHolderModule.ts similarity index 91% rename from packages/openid4vc-holder/src/OpenId4VcHolderModule.ts rename to packages/openid4vc-holder/src/OpenId4VciHolderModule.ts index 0fbedaf240..8488cdb9cc 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts +++ b/packages/openid4vc-holder/src/OpenId4VciHolderModule.ts @@ -2,8 +2,8 @@ import type { DependencyManager, Module } from '@aries-framework/core' import { AgentConfig } from '@aries-framework/core' -import { OpenId4VcHolderApi } from './OpenId4VcHolderApi' -import { OpenId4VcHolderService } from './OpenId4VcHolderService' +import { OpenId4VcHolderApi } from './OpenId4VciHolderApi' +import { OpenId4VcHolderService } from './OpenId4VciHolderService' import { PresentationExchangeService } from './presentations' import { OpenId4VpHolderService } from './presentations/OpenId4VpHolderService' diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderService.ts b/packages/openid4vc-holder/src/OpenId4VciHolderService.ts similarity index 99% rename from packages/openid4vc-holder/src/OpenId4VcHolderService.ts rename to packages/openid4vc-holder/src/OpenId4VciHolderService.ts index b6c5cde09b..128eaa84cc 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderService.ts +++ b/packages/openid4vc-holder/src/OpenId4VciHolderService.ts @@ -8,7 +8,7 @@ import type { SupportedCredentialFormats, ResolvedCredentialOffer, ResolvedAuthorizationRequest, -} from './OpenId4VcHolderServiceOptions' +} from './OpenId4VciHolderServiceOptions' import type { OpenIdCredentialFormatProfile } from './utils' import type { AgentContext, @@ -73,7 +73,7 @@ import { } from '@sphereon/oid4vci-common' import { randomStringForEntropy } from '@stablelib/random' -import { supportedCredentialFormats } from './OpenId4VcHolderServiceOptions' +import { supportedCredentialFormats } from './OpenId4VciHolderServiceOptions' import { fromOpenIdCredentialFormatProfileToDifClaimFormat } from './utils' import { getUniformFormat } from './utils/Formats' import { getSupportedCredentials } from './utils/IssuerMetadataUtils' diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts b/packages/openid4vc-holder/src/OpenId4VciHolderServiceOptions.ts similarity index 99% rename from packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts rename to packages/openid4vc-holder/src/OpenId4VciHolderServiceOptions.ts index 9b4d352d92..9e2af31a17 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts +++ b/packages/openid4vc-holder/src/OpenId4VciHolderServiceOptions.ts @@ -1,4 +1,4 @@ -import type { OfferedCredentialType } from './OpenId4VcHolderService' +import type { OfferedCredentialType } from './OpenId4VciHolderService' import type { JwaSignatureAlgorithm, KeyType, VerificationMethod } from '@aries-framework/core' import type { CredentialOfferPayloadV1_0_11, EndpointMetadataResult, OpenId4VCIVersion } from '@sphereon/oid4vci-common' import type { CredentialFormat } from '@sphereon/ssi-types' From 93d6219b9e6fcf79c04ee315801830c3c0036bf4 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 25 Oct 2023 16:37:05 +0200 Subject: [PATCH 021/115] refactor: restructure Signed-off-by: Martin Auer --- packages/openid4vc-holder/src/index.ts | 8 ++++---- .../src/{ => issuance}/OpenId4VciHolderApi.ts | 3 ++- .../src/{ => issuance}/OpenId4VciHolderModule.ts | 4 ++-- .../src/{ => issuance}/OpenId4VciHolderService.ts | 8 ++++---- .../src/{ => issuance}/OpenId4VciHolderServiceOptions.ts | 2 +- 5 files changed, 13 insertions(+), 12 deletions(-) rename packages/openid4vc-holder/src/{ => issuance}/OpenId4VciHolderApi.ts (97%) rename packages/openid4vc-holder/src/{ => issuance}/OpenId4VciHolderModule.ts (90%) rename packages/openid4vc-holder/src/{ => issuance}/OpenId4VciHolderService.ts (99%) rename packages/openid4vc-holder/src/{ => issuance}/OpenId4VciHolderServiceOptions.ts (99%) diff --git a/packages/openid4vc-holder/src/index.ts b/packages/openid4vc-holder/src/index.ts index 97dd2a45fb..54a12581e0 100644 --- a/packages/openid4vc-holder/src/index.ts +++ b/packages/openid4vc-holder/src/index.ts @@ -1,8 +1,8 @@ import 'fast-text-encoding' -export * from './OpenId4VcHolderApi' -export * from './OpenId4VcHolderModule' -export * from './OpenId4VcHolderService' +export * from './issuance/OpenId4VciHolderApi' +export * from './issuance/OpenId4VciHolderModule' +export * from './issuance/OpenId4VciHolderService' // Contains internal types, so we don't export everything export { AuthCodeFlowOptions, @@ -11,6 +11,6 @@ export { ProofOfPossessionVerificationMethodResolverOptions, RequestCredentialOptions, SupportedCredentialFormats, -} from './OpenId4VcHolderServiceOptions' +} from './issuance/OpenId4VciHolderServiceOptions' export * from './presentations' export { OpenIdCredentialFormatProfile } from './utils' diff --git a/packages/openid4vc-holder/src/OpenId4VciHolderApi.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderApi.ts similarity index 97% rename from packages/openid4vc-holder/src/OpenId4VciHolderApi.ts rename to packages/openid4vc-holder/src/issuance/OpenId4VciHolderApi.ts index 707fd97d6b..b32da5694c 100644 --- a/packages/openid4vc-holder/src/OpenId4VciHolderApi.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderApi.ts @@ -10,8 +10,9 @@ import type { CredentialOfferPayloadV1_0_11 } from '@sphereon/oid4vci-common' import { injectable, AgentContext } from '@aries-framework/core' +import { OpenId4VpHolderService } from '../presentations/OpenId4VpHolderService' + import { OpenId4VcHolderService } from './OpenId4VciHolderService' -import { OpenId4VpHolderService } from './presentations/OpenId4VpHolderService' /** * @public diff --git a/packages/openid4vc-holder/src/OpenId4VciHolderModule.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderModule.ts similarity index 90% rename from packages/openid4vc-holder/src/OpenId4VciHolderModule.ts rename to packages/openid4vc-holder/src/issuance/OpenId4VciHolderModule.ts index 8488cdb9cc..5e4c0c3f48 100644 --- a/packages/openid4vc-holder/src/OpenId4VciHolderModule.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderModule.ts @@ -4,8 +4,8 @@ import { AgentConfig } from '@aries-framework/core' import { OpenId4VcHolderApi } from './OpenId4VciHolderApi' import { OpenId4VcHolderService } from './OpenId4VciHolderService' -import { PresentationExchangeService } from './presentations' -import { OpenId4VpHolderService } from './presentations/OpenId4VpHolderService' +import { PresentationExchangeService } from '../presentations' +import { OpenId4VpHolderService } from '../presentations/OpenId4VpHolderService' /** * @public @module OpenId4VcHolderModule diff --git a/packages/openid4vc-holder/src/OpenId4VciHolderService.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts similarity index 99% rename from packages/openid4vc-holder/src/OpenId4VciHolderService.ts rename to packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts index 128eaa84cc..34f72386fb 100644 --- a/packages/openid4vc-holder/src/OpenId4VciHolderService.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts @@ -9,7 +9,7 @@ import type { ResolvedCredentialOffer, ResolvedAuthorizationRequest, } from './OpenId4VciHolderServiceOptions' -import type { OpenIdCredentialFormatProfile } from './utils' +import type { OpenIdCredentialFormatProfile } from '../utils' import type { AgentContext, JwaSignatureAlgorithm, @@ -74,9 +74,9 @@ import { import { randomStringForEntropy } from '@stablelib/random' import { supportedCredentialFormats } from './OpenId4VciHolderServiceOptions' -import { fromOpenIdCredentialFormatProfileToDifClaimFormat } from './utils' -import { getUniformFormat } from './utils/Formats' -import { getSupportedCredentials } from './utils/IssuerMetadataUtils' +import { fromOpenIdCredentialFormatProfileToDifClaimFormat } from '../utils' +import { getUniformFormat } from '../utils/Formats' +import { getSupportedCredentials } from '../utils/IssuerMetadataUtils' /** * The type of a credential offer entry. For each item in `credentials` array, the type MUST be one of the following: diff --git a/packages/openid4vc-holder/src/OpenId4VciHolderServiceOptions.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderServiceOptions.ts similarity index 99% rename from packages/openid4vc-holder/src/OpenId4VciHolderServiceOptions.ts rename to packages/openid4vc-holder/src/issuance/OpenId4VciHolderServiceOptions.ts index 9e2af31a17..1b8a213f01 100644 --- a/packages/openid4vc-holder/src/OpenId4VciHolderServiceOptions.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderServiceOptions.ts @@ -3,7 +3,7 @@ import type { JwaSignatureAlgorithm, KeyType, VerificationMethod } from '@aries- import type { CredentialOfferPayloadV1_0_11, EndpointMetadataResult, OpenId4VCIVersion } from '@sphereon/oid4vci-common' import type { CredentialFormat } from '@sphereon/ssi-types' -import { OpenIdCredentialFormatProfile } from './utils/claimFormatMapping' +import { OpenIdCredentialFormatProfile } from '../utils/claimFormatMapping' // TODO: use simpler object export interface AuthDetails { From b07981ac019d9ce0bc17fc341458f058bc6bfa52 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 25 Oct 2023 16:39:23 +0200 Subject: [PATCH 022/115] refactor: imports Signed-off-by: Martin Auer --- .../src/issuance/OpenId4VciHolderModule.ts | 5 +++-- .../src/issuance/OpenId4VciHolderService.ts | 7 ++----- .../openid4vc-holder/tests/OpenId4VcHolderModule.test.ts | 6 +++--- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderModule.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderModule.ts index 5e4c0c3f48..1a09cdb8dc 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderModule.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderModule.ts @@ -2,11 +2,12 @@ import type { DependencyManager, Module } from '@aries-framework/core' import { AgentConfig } from '@aries-framework/core' -import { OpenId4VcHolderApi } from './OpenId4VciHolderApi' -import { OpenId4VcHolderService } from './OpenId4VciHolderService' import { PresentationExchangeService } from '../presentations' import { OpenId4VpHolderService } from '../presentations/OpenId4VpHolderService' +import { OpenId4VcHolderApi } from './OpenId4VciHolderApi' +import { OpenId4VcHolderService } from './OpenId4VciHolderService' + /** * @public @module OpenId4VcHolderModule * This module provides the functionality to assume the role of owner in relation to the OpenId4VC specification suite. diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts index 34f72386fb..f7becb45ca 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts @@ -73,11 +73,12 @@ import { } from '@sphereon/oid4vci-common' import { randomStringForEntropy } from '@stablelib/random' -import { supportedCredentialFormats } from './OpenId4VciHolderServiceOptions' import { fromOpenIdCredentialFormatProfileToDifClaimFormat } from '../utils' import { getUniformFormat } from '../utils/Formats' import { getSupportedCredentials } from '../utils/IssuerMetadataUtils' +import { supportedCredentialFormats } from './OpenId4VciHolderServiceOptions' + /** * The type of a credential offer entry. For each item in `credentials` array, the type MUST be one of the following: * - CredentialSupported, when the value is a string and points to a credential from the `credentials_supported` array. @@ -92,10 +93,6 @@ export type OfferedCredentialsWithMetadata = | { credentialSupported: CredentialSupported; type: OfferedCredentialType.CredentialSupported } | { inlineCredentialOffer: CredentialOfferFormat; type: OfferedCredentialType.InlineCredentialOffer } -interface AcquireAuthorizationCodeResult { - code: string -} - interface AuthRequestOpts { credentialOffer: CredentialOfferPayloadV1_0_11 metadata: EndpointMetadataResult diff --git a/packages/openid4vc-holder/tests/OpenId4VcHolderModule.test.ts b/packages/openid4vc-holder/tests/OpenId4VcHolderModule.test.ts index f67cdbc991..b2fdd907cc 100644 --- a/packages/openid4vc-holder/tests/OpenId4VcHolderModule.test.ts +++ b/packages/openid4vc-holder/tests/OpenId4VcHolderModule.test.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/unbound-method */ import type { DependencyManager } from '@aries-framework/core' -import { OpenId4VcHolderApi } from '../src/OpenId4VcHolderApi' -import { OpenId4VcHolderModule } from '../src/OpenId4VcHolderModule' -import { OpenId4VcHolderService } from '../src/OpenId4VcHolderService' +import { OpenId4VcHolderApi } from '../src/issuance/OpenId4VciHolderApi' +import { OpenId4VcHolderModule } from '../src/issuance/OpenId4VciHolderModule' +import { OpenId4VcHolderService } from '../src/issuance/OpenId4VciHolderService' import { OpenId4VpHolderService, PresentationExchangeService } from '../src/presentations' const dependencyManager = { From 3e7b1623dafd738ca689f1ae1e015bd9a5e27f1d Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 25 Oct 2023 16:42:23 +0200 Subject: [PATCH 023/115] refactor: restructure Signed-off-by: Martin Auer --- packages/openid4vc-holder/src/index.ts | 2 +- .../src/issuance/OpenId4VciHolderService.ts | 8 ++++---- .../src/issuance/OpenId4VciHolderServiceOptions.ts | 2 +- .../openid4vc-holder/src/{ => issuance}/utils/Formats.ts | 0 .../src/{ => issuance}/utils/IssuerMetadataUtils.ts | 0 .../utils/__tests__/claimFormatMapping.test.ts | 0 .../src/{ => issuance}/utils/claimFormatMapping.ts | 0 .../openid4vc-holder/src/{ => issuance}/utils/index.ts | 0 8 files changed, 6 insertions(+), 6 deletions(-) rename packages/openid4vc-holder/src/{ => issuance}/utils/Formats.ts (100%) rename packages/openid4vc-holder/src/{ => issuance}/utils/IssuerMetadataUtils.ts (100%) rename packages/openid4vc-holder/src/{ => issuance}/utils/__tests__/claimFormatMapping.test.ts (100%) rename packages/openid4vc-holder/src/{ => issuance}/utils/claimFormatMapping.ts (100%) rename packages/openid4vc-holder/src/{ => issuance}/utils/index.ts (100%) diff --git a/packages/openid4vc-holder/src/index.ts b/packages/openid4vc-holder/src/index.ts index 54a12581e0..8234727681 100644 --- a/packages/openid4vc-holder/src/index.ts +++ b/packages/openid4vc-holder/src/index.ts @@ -13,4 +13,4 @@ export { SupportedCredentialFormats, } from './issuance/OpenId4VciHolderServiceOptions' export * from './presentations' -export { OpenIdCredentialFormatProfile } from './utils' +export { OpenIdCredentialFormatProfile } from './issuance/utils' diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts index f7becb45ca..0833703cf9 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts @@ -9,7 +9,7 @@ import type { ResolvedCredentialOffer, ResolvedAuthorizationRequest, } from './OpenId4VciHolderServiceOptions' -import type { OpenIdCredentialFormatProfile } from '../utils' +import type { OpenIdCredentialFormatProfile } from './utils' import type { AgentContext, JwaSignatureAlgorithm, @@ -73,9 +73,9 @@ import { } from '@sphereon/oid4vci-common' import { randomStringForEntropy } from '@stablelib/random' -import { fromOpenIdCredentialFormatProfileToDifClaimFormat } from '../utils' -import { getUniformFormat } from '../utils/Formats' -import { getSupportedCredentials } from '../utils/IssuerMetadataUtils' +import { fromOpenIdCredentialFormatProfileToDifClaimFormat } from './utils' +import { getUniformFormat } from './utils/Formats' +import { getSupportedCredentials } from './utils/IssuerMetadataUtils' import { supportedCredentialFormats } from './OpenId4VciHolderServiceOptions' diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderServiceOptions.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderServiceOptions.ts index 1b8a213f01..9e2af31a17 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderServiceOptions.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderServiceOptions.ts @@ -3,7 +3,7 @@ import type { JwaSignatureAlgorithm, KeyType, VerificationMethod } from '@aries- import type { CredentialOfferPayloadV1_0_11, EndpointMetadataResult, OpenId4VCIVersion } from '@sphereon/oid4vci-common' import type { CredentialFormat } from '@sphereon/ssi-types' -import { OpenIdCredentialFormatProfile } from '../utils/claimFormatMapping' +import { OpenIdCredentialFormatProfile } from './utils/claimFormatMapping' // TODO: use simpler object export interface AuthDetails { diff --git a/packages/openid4vc-holder/src/utils/Formats.ts b/packages/openid4vc-holder/src/issuance/utils/Formats.ts similarity index 100% rename from packages/openid4vc-holder/src/utils/Formats.ts rename to packages/openid4vc-holder/src/issuance/utils/Formats.ts diff --git a/packages/openid4vc-holder/src/utils/IssuerMetadataUtils.ts b/packages/openid4vc-holder/src/issuance/utils/IssuerMetadataUtils.ts similarity index 100% rename from packages/openid4vc-holder/src/utils/IssuerMetadataUtils.ts rename to packages/openid4vc-holder/src/issuance/utils/IssuerMetadataUtils.ts diff --git a/packages/openid4vc-holder/src/utils/__tests__/claimFormatMapping.test.ts b/packages/openid4vc-holder/src/issuance/utils/__tests__/claimFormatMapping.test.ts similarity index 100% rename from packages/openid4vc-holder/src/utils/__tests__/claimFormatMapping.test.ts rename to packages/openid4vc-holder/src/issuance/utils/__tests__/claimFormatMapping.test.ts diff --git a/packages/openid4vc-holder/src/utils/claimFormatMapping.ts b/packages/openid4vc-holder/src/issuance/utils/claimFormatMapping.ts similarity index 100% rename from packages/openid4vc-holder/src/utils/claimFormatMapping.ts rename to packages/openid4vc-holder/src/issuance/utils/claimFormatMapping.ts diff --git a/packages/openid4vc-holder/src/utils/index.ts b/packages/openid4vc-holder/src/issuance/utils/index.ts similarity index 100% rename from packages/openid4vc-holder/src/utils/index.ts rename to packages/openid4vc-holder/src/issuance/utils/index.ts From 3bf4ff78b924846d2a709deed8d58106c44c46a0 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Thu, 26 Oct 2023 11:01:15 +0200 Subject: [PATCH 024/115] refactor: holder vci side Signed-off-by: Martin Auer --- .../src/issuance/OpenId4VciHolderApi.ts | 40 +- .../src/issuance/OpenId4VciHolderService.ts | 479 ++++++------------ .../OpenId4VciHolderServiceOptions.ts | 2 +- .../src/issuance/utils/Formats.ts | 6 +- .../src/issuance/utils/IssuerMetadataUtils.ts | 145 +++++- 5 files changed, 330 insertions(+), 342 deletions(-) diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderApi.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderApi.ts index b32da5694c..cf22dc010e 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderApi.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderApi.ts @@ -47,32 +47,60 @@ export class OpenId4VcHolderApi { return resolved } + /** + * Resolves a credential offer given as payload, credential offer URL, or issuance initiation URL, + * into a unified format.` + * @param credentialOffer the credential offer to resolve + * @returns The uniform credential offer payload, the issuer metadata, protocol version, and credentials that can be requested. + */ public async resolveCredentialOffer(credentialOffer: string | CredentialOfferPayloadV1_0_11) { - const resolved = await this.openId4VcHolderService.resolveCredentialOffer(credentialOffer) - return resolved + const resolvedCredentialOffer = await this.openId4VcHolderService.resolveCredentialOffer(credentialOffer) + return resolvedCredentialOffer } + /** + * This function is to be used with the Authorization Code Flow. + * It will generate the authorization request URI based on the provided options. + * The authorization request URI is used to obtain the authorization code. Currently this needs to be done manually. + * @param resolvedCredentialOffer Obtained through @function resolveCredentialOffer + * @param authCodeFlowOptions + * @returns The authorization request URI alongside the code verifier and original @param authCodeFlowOptions + */ public async resolveAuthorizationRequest( resolvedCredentialOffer: ResolvedCredentialOffer, authCodeFlowOptions: AuthCodeFlowOptions ) { - const uri = await this.openId4VcHolderService.resolveAuthorizationRequest( + const resolvedAuthorizationRequest = await this.openId4VcHolderService.resolveAuthorizationRequest( resolvedCredentialOffer, authCodeFlowOptions ) - return uri + return resolvedAuthorizationRequest } + /** + * Accepts a credential offer using the pre-authorized code flow. + * @param resolvedCredentialOffer Obtained through @function resolveCredentialOffer + * @param acceptCredentialOfferOptions + * @returns W3cCredentialRecord[] + */ public async acceptCredentialOfferUsingPreAuthorizedCode( resolvedCredentialOffer: ResolvedCredentialOffer, - options: AcceptCredentialOfferOptions + acceptCredentialOfferOptions: AcceptCredentialOfferOptions ): Promise { return this.openId4VcHolderService.acceptCredentialOffer(this.agentContext, { resolvedCredentialOffer, - acceptCredentialOfferOptions: options, + acceptCredentialOfferOptions, }) } + /** + * Accepts a credential offer using the authorization code flow. + * @param resolvedCredentialOffer Obtained through @function resolveCredentialOffer + * @param resolvedAuthorizationRequest Obtained through @function resolveAuthorizationRequest + * @param code The authorization code obtained via the authorization request URI + * @param acceptCredentialOfferOptions + * @returns W3cCredentialRecord[] + */ public async acceptCredentialOfferUsingAuthorizationCode( resolvedCredentialOffer: ResolvedCredentialOffer, resolvedAuthorizationRequest: ResolvedAuthorizationRequest, diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts index 0833703cf9..4d37a432d6 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts @@ -10,6 +10,7 @@ import type { ResolvedAuthorizationRequest, } from './OpenId4VciHolderServiceOptions' import type { OpenIdCredentialFormatProfile } from './utils' +import type { OfferedCredentialWithMetadata } from './utils/IssuerMetadataUtils' import type { AgentContext, JwaSignatureAlgorithm, @@ -19,10 +20,8 @@ import type { } from '@aries-framework/core' import type { AccessTokenResponse, - CredentialOfferFormat, CredentialOfferPayloadV1_0_11, CredentialResponse, - CredentialSupported, EndpointMetadataResult, Jwt, OpenIDResponse, @@ -59,7 +58,6 @@ import { AccessTokenClient, CredentialOfferClient, CredentialRequestClientBuilder, - MetadataClient, ProofOfPossessionBuilder, formPost, } from '@sphereon/oid4vci-client' @@ -73,27 +71,17 @@ import { } from '@sphereon/oid4vci-common' import { randomStringForEntropy } from '@stablelib/random' +import { supportedCredentialFormats } from './OpenId4VciHolderServiceOptions' import { fromOpenIdCredentialFormatProfileToDifClaimFormat } from './utils' import { getUniformFormat } from './utils/Formats' -import { getSupportedCredentials } from './utils/IssuerMetadataUtils' - -import { supportedCredentialFormats } from './OpenId4VciHolderServiceOptions' - -/** - * The type of a credential offer entry. For each item in `credentials` array, the type MUST be one of the following: - * - CredentialSupported, when the value is a string and points to a credential from the `credentials_supported` array. - * - InlineCredentialOffer, when the value is a JSON object that represents an inline credential offer. - */ -export enum OfferedCredentialType { - CredentialSupported = 'CredentialSupported', - InlineCredentialOffer = 'InlineCredentialOffer', -} - -export type OfferedCredentialsWithMetadata = - | { credentialSupported: CredentialSupported; type: OfferedCredentialType.CredentialSupported } - | { inlineCredentialOffer: CredentialOfferFormat; type: OfferedCredentialType.InlineCredentialOffer } +import { + getMetadataFromCredentialOffer, + getOfferedCredentialsWithMetadata, + handleAuthorizationDetails, + OfferedCredentialType, +} from './utils/IssuerMetadataUtils' -interface AuthRequestOpts { +async function createAuthorizationRequestUri(options: { credentialOffer: CredentialOfferPayloadV1_0_11 metadata: EndpointMetadataResult clientId: string @@ -102,152 +90,88 @@ interface AuthRequestOpts { authDetails?: AuthDetails | AuthDetails[] redirectUri: string scope?: string[] -} +}) { + const { scope, authDetails, metadata, clientId, codeChallenge, codeChallengeMethod, redirectUri } = options + let nonEmptyScope = !scope || scope.length === 0 ? undefined : scope?.join(' ') + const nonEmptyAuthDetails = !authDetails || authDetails.length === 0 ? undefined : authDetails + + // Scope and authorization_details can be used in the same authorization request + // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param + if (!nonEmptyScope && !nonEmptyAuthDetails) { + throw new AriesFrameworkError('Please provide a scope or authorization_details') + } -/** - * @internal - */ -@injectable() -export class OpenId4VcHolderService { - private logger: Logger - private w3cCredentialService: W3cCredentialService - private jwsService: JwsService + // Authorization servers supporting PAR SHOULD include the URL of their pushed authorization request endpoint in their authorization server metadata document + // Note that the presence of pushed_authorization_request_endpoint is sufficient for a client to determine that it may use the PAR flow. + let parEndpoint = metadata.credentialIssuerMetadata?.pushed_authorization_request_endpoint + if (typeof parEndpoint !== 'string') parEndpoint = undefined - public constructor( - @inject(InjectionSymbols.Logger) logger: Logger, - w3cCredentialService: W3cCredentialService, - jwsService: JwsService - ) { - this.w3cCredentialService = w3cCredentialService - this.jwsService = jwsService - this.logger = logger - } + let authorizationEndpoint = metadata.credentialIssuerMetadata?.authorization_endpoint + if (typeof authorizationEndpoint !== 'string') authorizationEndpoint = undefined - // TODO: copied from sphereon - private handleAuthorizationDetails( - metadata: EndpointMetadataResult, - authorizationDetails?: AuthDetails | AuthDetails[] - ): AuthDetails | AuthDetails[] | undefined { - if (authorizationDetails) { - if (Array.isArray(authorizationDetails)) { - return authorizationDetails.map((value) => this.handleLocations({ ...value }, metadata)) - } else { - return this.handleLocations({ ...authorizationDetails }, metadata) - } - } - return authorizationDetails + if (!authorizationEndpoint && !parEndpoint) { + throw new AriesFrameworkError( + "Server metadata does not contain an 'authorization_endpoint' which is required for the 'Authorization Code Flow'" + ) } - // TODO copied from sphereon - private handleLocations(authorizationDetails: AuthDetails, metadata: EndpointMetadataResult) { - if ( - authorizationDetails && - (metadata.credentialIssuerMetadata?.authorization_server || metadata.authorization_endpoint) - ) { - if (authorizationDetails.locations) { - if (Array.isArray(authorizationDetails.locations)) { - ;(authorizationDetails.locations as string[]).push(metadata.issuer) - } else { - authorizationDetails.locations = [authorizationDetails.locations as string, metadata.issuer] - } - } else { - authorizationDetails.locations = metadata.issuer - } - } - return authorizationDetails + // add 'openid' scope if not present + if (!nonEmptyScope?.includes('openid')) { + nonEmptyScope = ['openid', nonEmptyScope].filter((s) => !!s).join(' ') } - // TODO: copied from sphereon - public async acquireAuthorizationRequestCode({ - credentialOffer, - metadata, - clientId, - codeChallengeMethod, - codeChallenge, - redirectUri, - scope: _scope, - authDetails: _authDetails, - }: AuthRequestOpts) { - let scope = !_scope || _scope.length === 0 ? undefined : _scope?.join(' ') - const authDetails = !_authDetails || _authDetails.length === 0 ? undefined : _authDetails - - // Scope and authorization_details can be used in the same authorization request - // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param - if (!scope && !authDetails) { - throw new AriesFrameworkError('Please provide a scope or authorization_details') - } - - // Authorization servers supporting PAR SHOULD include the URL of their pushed authorization request endpoint in their authorization server metadata document - // Note that the presence of pushed_authorization_request_endpoint is sufficient for a client to determine that it may use the PAR flow. - // What happens if it doesn't ??? - let parEndpoint = metadata.credentialIssuerMetadata?.pushed_authorization_request_endpoint - if (typeof parEndpoint !== 'string') parEndpoint = undefined - - let authorizationEndpoint = metadata.credentialIssuerMetadata?.authorization_endpoint - if (typeof authorizationEndpoint !== 'string') authorizationEndpoint = undefined + const queryObj: { [key: string]: string } = { + client_id: clientId, + response_type: ResponseType.AUTH_CODE, + code_challenge_method: codeChallengeMethod, + code_challenge: codeChallenge, + redirect_uri: redirectUri, + } - if (!authorizationEndpoint) { - throw new AriesFrameworkError( - "Server metadata does not contain 'authorization_endpoint'. Which is required for the Authorization Code Flow" - ) - } + if (nonEmptyScope) queryObj['scope'] = nonEmptyScope - // add 'openid' scope if not present - if (!scope?.includes('openid')) { - scope = ['openid', scope].filter((s) => !!s).join(' ') - } + if (nonEmptyAuthDetails) + queryObj['authorization_details'] = JSON.stringify(handleAuthorizationDetails(nonEmptyAuthDetails, metadata)) - const queryObj: { [key: string]: string } = { - client_id: clientId, - response_type: ResponseType.AUTH_CODE, - code_challenge_method: codeChallengeMethod, - code_challenge: codeChallenge, - redirect_uri: redirectUri, - } + const issuerState = options.credentialOffer.grants?.authorization_code?.issuer_state + if (issuerState) queryObj['issuer_state'] = issuerState - if (scope) queryObj['scope'] = scope - - const authorizationDetails = JSON.stringify(this.handleAuthorizationDetails(metadata, authDetails)) - if (authorizationDetails) queryObj['authorization_details'] = authorizationDetails - - const issuerState = credentialOffer.grants?.authorization_code?.issuer_state - if (issuerState) queryObj['issuer_state'] = issuerState - - if (parEndpoint) { - const body = new URLSearchParams(queryObj) - const response = await formPost(parEndpoint, body) - if (!response.successBody) - throw new AriesFrameworkError(`Could not acquire the authorization request uri from '${parEndpoint}'`) - return convertJsonToURI( - { request_uri: response.successBody.request_uri, client_id: clientId, response_type: ResponseType.AUTH_CODE }, - { - baseUrl: authorizationEndpoint, - uriTypeProperties: ['request_uri', 'client_id', 'response_type'], - mode: JsonURIMode.X_FORM_WWW_URLENCODED, - } - ) - } else { - return convertJsonToURI(queryObj, { + if (parEndpoint) { + const body = new URLSearchParams(queryObj) + const response = await formPost(parEndpoint, body) + if (!response.successBody) + throw new AriesFrameworkError(`Could not acquire the authorization request uri from '${parEndpoint}'`) + return convertJsonToURI( + { request_uri: response.successBody.request_uri, client_id: clientId, response_type: ResponseType.AUTH_CODE }, + { baseUrl: authorizationEndpoint, - uriTypeProperties: ['redirect_uri', 'scope', 'authorization_details', 'issuer_state'], + uriTypeProperties: ['request_uri', 'client_id', 'response_type'], mode: JsonURIMode.X_FORM_WWW_URLENCODED, - }) - } + } + ) + } else { + return convertJsonToURI(queryObj, { + baseUrl: authorizationEndpoint, + uriTypeProperties: ['redirect_uri', 'scope', 'authorization_details', 'issuer_state'], + mode: JsonURIMode.X_FORM_WWW_URLENCODED, + }) } +} - private getFormatAndTypesFromOfferedCredential( - offeredCredential: OfferedCredentialsWithMetadata, - version: OpenId4VCIVersion +@injectable() +export class OpenId4VcHolderService { + private logger: Logger + private w3cCredentialService: W3cCredentialService + private jwsService: JwsService + + public constructor( + @inject(InjectionSymbols.Logger) logger: Logger, + w3cCredentialService: W3cCredentialService, + jwsService: JwsService ) { - if (offeredCredential.type === OfferedCredentialType.InlineCredentialOffer) { - const { format, types } = offeredCredential.inlineCredentialOffer - return { format: format as SupportedCredentialFormats, types } - } else { - const { format, types } = offeredCredential.credentialSupported - const uniFormat = - version < OpenId4VCIVersion.VER_1_0_11 ? (getUniformFormat(format) as SupportedCredentialFormats) : format - return { format: uniFormat, types } - } + this.w3cCredentialService = w3cCredentialService + this.jwsService = jwsService + this.logger = logger } public async resolveCredentialOffer( @@ -274,10 +198,7 @@ export class OpenId4VcHolderService { } const credentialOfferPayload = (await assertedUniformCredentialOffer(uniformCredentialOffer)).credential_offer - const issuer = credentialOfferPayload.credential_issuer - - const metadata = await MetadataClient.retrieveAllMetadata(issuer) - if (!metadata) throw new AriesFrameworkError(`Could not retrieve metadata for OpenID4VCI issuer: ${issuer}`) + const { metadata, issuerMetadata } = await getMetadataFromCredentialOffer(credentialOfferPayload) this.logger.info('Fetched server metadata', { issuer: metadata.issuer, @@ -287,16 +208,14 @@ export class OpenId4VcHolderService { this.logger.debug('Full server metadata', metadata) - const offeredCredentialsWithMetadata = this.getOfferedCredentialsWithMetadata( + const offeredCredentialsWithMetadata = getOfferedCredentialsWithMetadata( credentialOfferPayload, - metadata.credentialIssuerMetadata, + issuerMetadata, version ) const credentialsToRequest: CredentialToRequest[] = offeredCredentialsWithMetadata.map((offeredCredential) => { - const { format, types } = this.getFormatAndTypesFromOfferedCredential(offeredCredential, version) - const offerType = offeredCredential.type - + const { format, types, offerType } = offeredCredential if (offerType === OfferedCredentialType.InlineCredentialOffer) { return { offerType, types, format } } else { @@ -320,17 +239,13 @@ export class OpenId4VcHolderService { authCodeFlowOptions: AuthCodeFlowOptions ): Promise { const { credentialOfferPayload, metadata: _metadata } = resolvedCredentialOffer - - // TODO: authdetails - - const issuer = credentialOfferPayload.credential_issuer - const metadata = _metadata ? _metadata : await MetadataClient.retrieveAllMetadata(issuer) - if (!metadata) throw new AriesFrameworkError(`Could not retrieve metadata for OpenID4VCI issuer: ${issuer}`) - + const { metadata } = await getMetadataFromCredentialOffer(credentialOfferPayload, _metadata) const codeVerifier = randomStringForEntropy(256) const codeVerifierSha256 = Hasher.hash(TypedArrayEncoder.fromString(codeVerifier), 'sha2-256') const codeChallenge = TypedArrayEncoder.toBase64URL(codeVerifierSha256) + // TODO: authdetails + this.logger.debug('Converted code_verifier to code_challenge', { codeVerifier: codeVerifier, sha256: codeVerifierSha256.toString(), @@ -338,7 +253,7 @@ export class OpenId4VcHolderService { }) const { clientId, redirectUri, scope, authDetails } = authCodeFlowOptions - const authorizationRequestUri = await this.acquireAuthorizationRequestCode({ + const authorizationRequestUri = await createAuthorizationRequestUri({ credentialOffer: credentialOfferPayload, clientId, codeChallengeMethod: CodeChallengeMethod.SHA256, @@ -365,15 +280,9 @@ export class OpenId4VcHolderService { } ) { const { resolvedCredentialOffer, acceptCredentialOfferOptions, resolvedAuthorizationRequest } = options - - const { - credentialsToRequest, - allowedProofOfPossessionSignatureAlgorithms: _allowedProofOfPossessionSignatureAlgorithms, - userPin, - proofOfPossessionVerificationMethodResolver, - verifyCredentialStatus, - } = acceptCredentialOfferOptions const { credentialOfferPayload, metadata: _metadata, version } = resolvedCredentialOffer + const { credentialsToRequest, userPin, proofOfPossessionVerificationMethodResolver, verifyCredentialStatus } = + acceptCredentialOfferOptions if (credentialsToRequest?.length === 0) { this.logger.warn(`Accepting 0 credential offers. Returning`) @@ -382,55 +291,49 @@ export class OpenId4VcHolderService { this.logger.info(`Accepting the following credential offers '${credentialsToRequest}'`) - const issuer = credentialOfferPayload.credential_issuer - const metadata = _metadata ? _metadata : await MetadataClient.retrieveAllMetadata(issuer) - if (!metadata) throw new AriesFrameworkError(`Could not retrieve metadata for OpenID4VCI issuer: ${issuer}`) - - const issuerMetadata = metadata.credentialIssuerMetadata - if (!issuerMetadata) throw new AriesFrameworkError('Found no credential issuer metadata') - + const { metadata, issuerMetadata } = await getMetadataFromCredentialOffer(credentialOfferPayload, _metadata) const supportedJwaSignatureAlgorithms = this.getSupportedJwaSignatureAlgorithms(agentContext) - const allowedProofOfPossessionSignatureAlgorithms = _allowedProofOfPossessionSignatureAlgorithms - ? _allowedProofOfPossessionSignatureAlgorithms.filter((algorithm) => - supportedJwaSignatureAlgorithms.includes(algorithm) - ) + const possibleProofOfPossessionSigAlgs = acceptCredentialOfferOptions.allowedProofOfPossessionSignatureAlgorithms + const allowedProofOfPossessionSignatureAlgorithms = possibleProofOfPossessionSigAlgs + ? possibleProofOfPossessionSigAlgs.filter((algorithm) => supportedJwaSignatureAlgorithms.includes(algorithm)) : supportedJwaSignatureAlgorithms if (allowedProofOfPossessionSignatureAlgorithms.length === 0) { - throw new AriesFrameworkError(`No supported proof of possession signature algorithms found.`) + throw new AriesFrameworkError(`No supported proof of possession signature algorithm found.`) } // acquire the access token - let openIdAccessTokenResponse: OpenIDResponse + let accessTokenResponse: OpenIDResponse const accessTokenClient = new AccessTokenClient() if (resolvedAuthorizationRequest) { const { code, codeVerifier, redirectUri } = resolvedAuthorizationRequest - openIdAccessTokenResponse = await accessTokenClient.acquireAccessToken({ + accessTokenResponse = await accessTokenClient.acquireAccessToken({ metadata, credentialOffer: { credential_offer: credentialOfferPayload }, + pin: userPin, code, codeVerifier, redirectUri, - pin: userPin, }) } else { - openIdAccessTokenResponse = await accessTokenClient.acquireAccessToken({ + accessTokenResponse = await accessTokenClient.acquireAccessToken({ metadata, credentialOffer: { credential_offer: credentialOfferPayload }, pin: userPin, }) } - if (!openIdAccessTokenResponse.successBody) { - throw new AriesFrameworkError(`could not acquire access token from '${metadata.issuer}'`) + if (!accessTokenResponse.successBody) { + throw new AriesFrameworkError(`could not acquire access token from '${metadata.issuer}'.`) } - this.logger.debug('Requested OpenId4VCI Access Token') - const accessToken = openIdAccessTokenResponse.successBody + this.logger.debug('Requested OpenId4VCI Access Token.') - const offeredCredentialsWithMetadata = this.getOfferedCredentialsWithMetadata( + const accessToken = accessTokenResponse.successBody + + const offeredCredentialsWithMetadata = getOfferedCredentialsWithMetadata( credentialOfferPayload, issuerMetadata, version @@ -438,13 +341,14 @@ export class OpenId4VcHolderService { const credentialsToRequestWithMetadata = credentialsToRequest?.map((ctr) => { const credentialToRequest = offeredCredentialsWithMetadata.find((offeredCredentialWithMetadata) => { - const { format, types } = this.getFormatAndTypesFromOfferedCredential(offeredCredentialWithMetadata, version) + const { format, types } = offeredCredentialWithMetadata + // only requests credentials with the exact same set of types and format return ctr.format === format && ctr.types.sort().join(',') === types.sort().join(',') }) if (!credentialToRequest) throw new AriesFrameworkError( - `Could not find the the requested credential with format '${ctr.format}' and types '${ctr.types}' in the offered credentials` + `Could not find the the requested credential with format '${ctr.format}' and types '${ctr.types}' in the offered credentials.` ) return credentialToRequest @@ -459,7 +363,7 @@ export class OpenId4VcHolderService { allowedCredentialFormats: supportedCredentialFormats, allowedProofOfPossessionSignatureAlgorithms, offeredCredentialWithMetadata: credentialWithMetadata, - proofOfPossessionVerificationMethodResolver: proofOfPossessionVerificationMethodResolver, + proofOfPossessionVerificationMethodResolver, }) const callbacks: ProofOfPossessionCallbacks = { @@ -487,11 +391,8 @@ export class OpenId4VcHolderService { .withCredentialEndpoint(metadata.credential_endpoint) .withTokenFromResponse(accessToken) - const isInlineOffer = isInlineCredentialOffer(credentialWithMetadata) - - const format = isInlineOffer - ? credentialWithMetadata.inlineCredentialOffer.format - : credentialWithMetadata.credentialSupported.format + const isInlineOffer = credentialWithMetadata.offerType === OfferedCredentialType.InlineCredentialOffer + const format = credentialWithMetadata.format let credentialTypes: string | string[] if (version < OpenId4VCIVersion.VER_1_0_11) { @@ -519,12 +420,7 @@ export class OpenId4VcHolderService { }) // Create credential record, but we don't store it yet (only after the user has accepted the credential) - const credentialRecord = new W3cCredentialRecord({ - credential, - tags: { - expandedTypes: [], - }, - }) + const credentialRecord = new W3cCredentialRecord({ credential, tags: { expandedTypes: [] } }) this.logger.debug('Full credential', credentialRecord) receivedCredentials.push(credentialRecord) @@ -545,7 +441,7 @@ export class OpenId4VcHolderService { proofOfPossessionVerificationMethodResolver: ProofOfPossessionVerificationMethodResolver allowedCredentialFormats: SupportedCredentialFormats[] allowedProofOfPossessionSignatureAlgorithms: JwaSignatureAlgorithm[] - offeredCredentialWithMetadata: OfferedCredentialsWithMetadata + offeredCredentialWithMetadata: OfferedCredentialWithMetadata } ) { const { signatureAlgorithm, supportedDidMethods, supportsAllDidMethods } = this.getProofOfPossessionRequirements( @@ -558,20 +454,15 @@ export class OpenId4VcHolderService { ) const JwkClass = getJwkClassFromJwaSignatureAlgorithm(signatureAlgorithm) - if (!JwkClass) { throw new AriesFrameworkError( - `Could not determine JWK key type based on JWA signature algorithm '${signatureAlgorithm}'` + `Could not determine JWK key type of the JWA signature algorithm '${signatureAlgorithm}'` ) } const supportedVerificationMethods = getSupportedVerificationMethodTypesFromKeyType(JwkClass.keyType) - const format = ( - isInlineCredentialOffer(options.offeredCredentialWithMetadata) - ? options.offeredCredentialWithMetadata.inlineCredentialOffer.format - : options.offeredCredentialWithMetadata.credentialSupported.format - ) as SupportedCredentialFormats + const format = options.offeredCredentialWithMetadata.format as SupportedCredentialFormats // Now we need to determine the did method and alg based on the cryptographic suite const verificationMethod = await options.proofOfPossessionVerificationMethodResolver({ @@ -579,7 +470,9 @@ export class OpenId4VcHolderService { proofOfPossessionSignatureAlgorithm: signatureAlgorithm, supportedVerificationMethods, keyType: JwkClass.keyType, - supportedCredentialId: !isInlineCredentialOffer(options.offeredCredentialWithMetadata) + supportedCredentialId: !( + options.offeredCredentialWithMetadata.offerType === OfferedCredentialType.InlineCredentialOffer + ) ? options.offeredCredentialWithMetadata.credentialSupported.id : undefined, supportsAllDidMethods, @@ -612,58 +505,6 @@ export class OpenId4VcHolderService { return { verificationMethod, signatureAlgorithm } } - /** - * Returns all entries from the credential offer with the associated metadata resolved. For inline entries, the offered credential object - * is included directly. For 'id' entries, the associated `credentials_supported` object is resolved from the issuer metadata. - * - * NOTE: for v1_0-08, a single credential id in the issuer metadata could have multiple formats. This means that the returned value - * from this method could contain multiple entries for a single credential id, but with different formats. This is detectable as the - * id will be the `-`. - */ - private getOfferedCredentialsWithMetadata = ( - credentialOfferPayload: CredentialOfferPayloadV1_0_11, - issuerMetadata: EndpointMetadataResult['credentialIssuerMetadata'], - version: OpenId4VCIVersion - ) => { - const offeredCredentials: Array = [] - - const supportedCredentials = getSupportedCredentials({ issuerMetadata, version }) - - for (const offeredCredential of credentialOfferPayload.credentials) { - // If the offeredCredential is a string, it has to reference a supported credential in the issuer metadata - if (typeof offeredCredential === 'string') { - const foundSupportedCredentials = supportedCredentials.filter( - (supportedCredential) => - supportedCredential.id === offeredCredential || - supportedCredential.id === `${offeredCredential}-${supportedCredential.format}` - ) - - // Make sure the issuer metadata includes the offered credential. - if (foundSupportedCredentials.length === 0) { - throw new Error( - `Offered credential '${offeredCredential}' is not part of credentials_supported of the issuer metadata` - ) - } - - for (const foundSupportedCredential of foundSupportedCredentials) { - offeredCredentials.push({ - credentialSupported: foundSupportedCredential, - type: OfferedCredentialType.CredentialSupported, - } as const) - } - } - // Otherwise it's an inline credential offer that does not reference a supported credential in the issuer metadata - else { - offeredCredentials.push({ - inlineCredentialOffer: offeredCredential, - type: OfferedCredentialType.InlineCredentialOffer, - } as const) - } - } - - return offeredCredentials - } - /** * Get the requirements for creating the proof of possession. Based on the allowed * credential formats, the allowed proof of possession signature algorithms, and the @@ -673,21 +514,14 @@ export class OpenId4VcHolderService { private getProofOfPossessionRequirements( agentContext: AgentContext, options: { - offeredCredentialWithMetadata: OfferedCredentialsWithMetadata + offeredCredentialWithMetadata: OfferedCredentialWithMetadata allowedCredentialFormats: SupportedCredentialFormats[] allowedProofOfPossessionSignatureAlgorithms: JwaSignatureAlgorithm[] } ): ProofOfPossessionRequirements { const { offeredCredentialWithMetadata, allowedCredentialFormats } = options - const isInlineOffer = offeredCredentialWithMetadata.type === OfferedCredentialType.InlineCredentialOffer - - // Extract format from offer - let format = isInlineOffer - ? offeredCredentialWithMetadata.inlineCredentialOffer.format - : offeredCredentialWithMetadata.credentialSupported.format - - // Get uniform format, so we don't have to deal with the different spec versions - format = getUniformFormat(format) + const isInlineOffer = offeredCredentialWithMetadata.offerType === OfferedCredentialType.InlineCredentialOffer + const format = offeredCredentialWithMetadata.format const credentialSupportedMetadata = isInlineOffer ? undefined : offeredCredentialWithMetadata.credentialSupported @@ -703,9 +537,11 @@ export class OpenId4VcHolderService { const credentialMetadata = offeredCredentialWithMetadata.credentialSupported if (!allowedCredentialFormats.includes(format as SupportedCredentialFormats)) { throw new AriesFrameworkError( - `Issuer only supports format '${format}' for credential type '${credentialMetadata.types.join( - ', ' - )}', but the wallet only allows formats '${options.allowedCredentialFormats.join(', ')}'` + [ + `The issuer only supports format '${format}'`, + `for the credential type '${credentialMetadata.types.join(', ')}`, + `but the wallet only allows formats '${options.allowedCredentialFormats.join(', ')}'`, + ].join(' ') ) } } @@ -713,53 +549,38 @@ export class OpenId4VcHolderService { // For each of the supported algs, find the key types, then find the proof types const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) - let potentialSignatureAlgorithm: JwaSignatureAlgorithm | undefined + let signatureAlgorithm: JwaSignatureAlgorithm | undefined + + // If undefined, it means the issuer didn't include the cryptographic suites in the metadata + // We just guess that the first one is supported + if (issuerSupportedCryptographicSuites === undefined) { + signatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms[0] + } switch (format) { case 'jwt_vc_json': case 'jwt_vc_json-ld': - // If undefined, it means the issuer didn't include the cryptographic suites in the metadata - // We just guess that the first one is supported - if (issuerSupportedCryptographicSuites === undefined) { - potentialSignatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms[0] - } else { - potentialSignatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms.find((signatureAlgorithm) => - issuerSupportedCryptographicSuites.includes(signatureAlgorithm) - ) - } + signatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms.find((signatureAlgorithm) => + issuerSupportedCryptographicSuites?.includes(signatureAlgorithm) + ) break case 'ldp_vc': - // If undefined, it means the issuer didn't include the cryptographic suites in the metadata - // We just guess that the first one is supported - if (issuerSupportedCryptographicSuites === undefined) { - potentialSignatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms[0] - } else { - // We need to find it based on the JSON-LD proof type - potentialSignatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms.find( - (signatureAlgorithm) => { - const JwkClass = getJwkClassFromJwaSignatureAlgorithm(signatureAlgorithm) - if (!JwkClass) return false - - const matchingSuite = signatureSuiteRegistry.getByKeyType(JwkClass.keyType) - if (matchingSuite.length === 0) return false - - return issuerSupportedCryptographicSuites.includes(matchingSuite[0].proofType) - } - ) - } + // We need to find it based on the JSON-LD proof type + signatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms.find((signatureAlgorithm) => { + const JwkClass = getJwkClassFromJwaSignatureAlgorithm(signatureAlgorithm) + if (!JwkClass) return false + + const matchingSuite = signatureSuiteRegistry.getByKeyType(JwkClass.keyType) + if (matchingSuite.length === 0) return false + + return issuerSupportedCryptographicSuites?.includes(matchingSuite[0].proofType) + }) break default: - throw new AriesFrameworkError( - `Unsupported requested credential format '${format}' with id ${ - credentialSupportedMetadata?.id ?? 'Inline credential offer' - }` - ) + throw new AriesFrameworkError(`Unsupported credential format. Requested format '${format}'`) } - const supportsAllDidMethods = issuerSupportedBindingMethods?.includes('did') ?? false - const supportedDidMethods = issuerSupportedBindingMethods?.filter((method) => method.startsWith('did:')) - - if (!potentialSignatureAlgorithm) { + if (!signatureAlgorithm) { throw new AriesFrameworkError( `Could not establish signature algorithm for format ${format} and id ${ credentialSupportedMetadata?.id ?? 'Inline credential offer' @@ -767,8 +588,11 @@ export class OpenId4VcHolderService { ) } + const supportsAllDidMethods = issuerSupportedBindingMethods?.includes('did') ?? false + const supportedDidMethods = issuerSupportedBindingMethods?.filter((method) => method.startsWith('did:')) + return { - signatureAlgorithm: potentialSignatureAlgorithm, + signatureAlgorithm, supportedDidMethods, supportsAllDidMethods, } @@ -859,8 +683,10 @@ export class OpenId4VcHolderService { const payload = JsonEncoder.toBuffer(jwt.payload) + // TODO: should we support JWK? // We don't support these properties, remove them, so we can pass all other header properties to the JWS service if (jwt.header.x5c || jwt.header.jwk) throw new AriesFrameworkError('x5c and jwk are not supported') + // eslint-disable-next-line @typescript-eslint/no-unused-vars const { x5c: _x5c, jwk: _jwk, ...supportedHeaderOptions } = jwt.header @@ -874,10 +700,3 @@ export class OpenId4VcHolderService { } } } - -function isInlineCredentialOffer(offeredCredential: OfferedCredentialsWithMetadata): offeredCredential is { - inlineCredentialOffer: CredentialOfferFormat - type: OfferedCredentialType.InlineCredentialOffer -} { - return offeredCredential.type === OfferedCredentialType.InlineCredentialOffer -} diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderServiceOptions.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderServiceOptions.ts index 9e2af31a17..c7d58528b9 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderServiceOptions.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderServiceOptions.ts @@ -1,4 +1,4 @@ -import type { OfferedCredentialType } from './OpenId4VciHolderService' +import type { OfferedCredentialType } from './utils/IssuerMetadataUtils' import type { JwaSignatureAlgorithm, KeyType, VerificationMethod } from '@aries-framework/core' import type { CredentialOfferPayloadV1_0_11, EndpointMetadataResult, OpenId4VCIVersion } from '@sphereon/oid4vci-common' import type { CredentialFormat } from '@sphereon/ssi-types' diff --git a/packages/openid4vc-holder/src/issuance/utils/Formats.ts b/packages/openid4vc-holder/src/issuance/utils/Formats.ts index 7d13fa6ef9..f529ad0620 100644 --- a/packages/openid4vc-holder/src/issuance/utils/Formats.ts +++ b/packages/openid4vc-holder/src/issuance/utils/Formats.ts @@ -11,9 +11,7 @@ const isUniformFormat = (format: string): format is OID4VCICredentialFormat => { export function getUniformFormat(format: string | OID4VCICredentialFormat | CredentialFormat): OID4VCICredentialFormat { // Already valid format - if (isUniformFormat(format)) { - return format - } + if (isUniformFormat(format)) return format // Older formats if (format === 'jwt_vc' || format === 'jwt') { @@ -27,7 +25,7 @@ export function getUniformFormat(format: string | OID4VCICredentialFormat | Cred } export function getFormatForVersion(format: string, version: OpenId4VCIVersion) { - const uniformFormat = isUniformFormat(format) ? format : getUniformFormat(format) + const uniformFormat = getUniformFormat(format) if (version < OpenId4VCIVersion.VER_1_0_11) { if (uniformFormat === 'jwt_vc_json') { diff --git a/packages/openid4vc-holder/src/issuance/utils/IssuerMetadataUtils.ts b/packages/openid4vc-holder/src/issuance/utils/IssuerMetadataUtils.ts index 3fda40e8d1..8fb1ae4065 100644 --- a/packages/openid4vc-holder/src/issuance/utils/IssuerMetadataUtils.ts +++ b/packages/openid4vc-holder/src/issuance/utils/IssuerMetadataUtils.ts @@ -1,16 +1,124 @@ +import type { AuthDetails } from '../OpenId4VciHolderServiceOptions' import type { CredentialIssuerMetadata, + CredentialOfferFormat, + CredentialOfferPayloadV1_0_11, CredentialSupported, CredentialSupportedTypeV1_0_08, CredentialSupportedV1_0_08, + EndpointMetadataResult, IssuerMetadataV1_0_08, MetadataDisplay, + OID4VCICredentialFormat, } from '@sphereon/oid4vci-common' +import { AriesFrameworkError } from '@aries-framework/core' +import { MetadataClient } from '@sphereon/oid4vci-client' import { OpenId4VCIVersion } from '@sphereon/oid4vci-common' +import { getUniformFormat } from './Formats' + +/** + * The type of a credential offer entry. For each item in `credentials` array, the type MUST be one of the following: + * - CredentialSupported, when the value is a string and points to a credential from the `credentials_supported` array. + * - InlineCredentialOffer, when the value is a JSON object that represents an inline credential offer. + */ +export enum OfferedCredentialType { + CredentialSupported = 'CredentialSupported', + InlineCredentialOffer = 'InlineCredentialOffer', +} + +export type OfferedCredentialWithMetadata = + | { + credentialSupported: CredentialSupported + offerType: OfferedCredentialType.CredentialSupported + format: OID4VCICredentialFormat + types: string[] + } + | { + inlineCredentialOffer: CredentialOfferFormat + offerType: OfferedCredentialType.InlineCredentialOffer + format: OID4VCICredentialFormat + types: string[] + } + +/** + * Returns all entries from the credential offer with the associated metadata resolved. For inline entries, the offered credential object + * is included directly. For 'id' entries, the associated `credentials_supported` object is resolved from the issuer metadata. + * + * NOTE: for v1_0-08, a single credential id in the issuer metadata could have multiple formats. This means that the returned value + * from this method could contain multiple entries for a single credential id, but with different formats. This is detectable as the + * id will be the `-`. + */ +export function getOfferedCredentialsWithMetadata( + credentialOfferPayload: CredentialOfferPayloadV1_0_11, + issuerMetadata: CredentialIssuerMetadata | IssuerMetadataV1_0_08, + version: OpenId4VCIVersion +) { + const offeredCredentials: OfferedCredentialWithMetadata[] = [] + + const supportedCredentials = getSupportedCredentials({ issuerMetadata, version }) + + for (const offeredCredential of credentialOfferPayload.credentials) { + // If the offeredCredential is a string, it has to reference a supported credential in the issuer metadata + if (typeof offeredCredential === 'string') { + const foundSupportedCredentials = supportedCredentials.filter( + (supportedCredential) => + supportedCredential.id === offeredCredential || + supportedCredential.id === `${offeredCredential}-${supportedCredential.format}` + ) + + // Make sure the issuer metadata includes the offered credential. + if (foundSupportedCredentials.length === 0) { + throw new Error( + `Offered credential '${offeredCredential}' is not part of credentials_supported of the issuer metadata.` + ) + } + + // TODO: use getUniFormat?? + + for (const foundSupportedCredential of foundSupportedCredentials) { + offeredCredentials.push({ + credentialSupported: foundSupportedCredential, + offerType: OfferedCredentialType.CredentialSupported, + format: getUniformFormat(foundSupportedCredential.format), + types: foundSupportedCredential.types, + }) + } + } + // Otherwise it's an inline credential offer that does not reference a supported credential in the issuer metadata + else { + offeredCredentials.push({ + inlineCredentialOffer: offeredCredential, + offerType: OfferedCredentialType.InlineCredentialOffer, + format: getUniformFormat(offeredCredential.format), + types: offeredCredential.types, + }) + } + } + + return offeredCredentials +} + +export async function getMetadataFromCredentialOffer( + credentialOfferPayload: CredentialOfferPayloadV1_0_11, + _metadata?: EndpointMetadataResult +) { + const issuer = credentialOfferPayload.credential_issuer + + const metadata = + _metadata && _metadata.credentialIssuerMetadata ? _metadata : await MetadataClient.retrieveAllMetadata(issuer) + if (!metadata) throw new AriesFrameworkError(`Could not retrieve metadata for OpenId4Vci issuer: ${issuer}`) + + const issuerMetadata = metadata.credentialIssuerMetadata + if (!issuerMetadata) + throw new AriesFrameworkError(`Could not retrieve issuer metadata for OpenId4Vci issuer: ${issuer}`) + + return { issuer, metadata, issuerMetadata } +} + export function getSupportedCredentials(opts?: { - issuerMetadata?: CredentialIssuerMetadata | IssuerMetadataV1_0_08 + issuerMetadata: CredentialIssuerMetadata | IssuerMetadataV1_0_08 version: OpenId4VCIVersion }): CredentialSupported[] { const { issuerMetadata } = opts ?? {} @@ -74,6 +182,41 @@ export function credentialSupportedV8ToV11( }) } +// copied from sphereon +export function handleAuthorizationDetails( + authorizationDetails: AuthDetails | AuthDetails[], + metadata: EndpointMetadataResult +): AuthDetails | AuthDetails[] | undefined { + if (authorizationDetails) { + if (Array.isArray(authorizationDetails)) { + return authorizationDetails.map((value) => handleLocations({ ...value }, metadata)) + } else { + return handleLocations({ ...authorizationDetails }, metadata) + } + } + return authorizationDetails +} + +// copied from sphereon +export function handleLocations(authorizationDetails: AuthDetails, metadata: EndpointMetadataResult) { + if ( + authorizationDetails && + (metadata.credentialIssuerMetadata?.authorization_server || metadata.authorization_endpoint) + ) { + if (authorizationDetails.locations) { + if (Array.isArray(authorizationDetails.locations)) { + ;(authorizationDetails.locations as string[]).push(metadata.issuer) + } else { + authorizationDetails.locations = [authorizationDetails.locations as string, metadata.issuer] + } + } else { + authorizationDetails.locations = metadata.issuer + } + } + return authorizationDetails +} + +// TODO export function getIssuerDisplays( metadata: CredentialIssuerMetadata | IssuerMetadataV1_0_08, opts?: { prefLocales: string[] } From 8f07645e512acb91e9a04913f898dff6d5ac1da7 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Thu, 26 Oct 2023 12:46:45 +0200 Subject: [PATCH 025/115] fix: format and tests Signed-off-by: Martin Auer --- .../src/issuance/OpenId4VciHolderService.ts | 53 +++---- packages/openid4vc-holder/tests/fixtures.ts | 61 ++++++++ .../tests/openid4vci-holder.e2e.test.ts | 142 ++++++++++++------ 3 files changed, 184 insertions(+), 72 deletions(-) diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts index 4d37a432d6..904863f5a9 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts @@ -73,7 +73,7 @@ import { randomStringForEntropy } from '@stablelib/random' import { supportedCredentialFormats } from './OpenId4VciHolderServiceOptions' import { fromOpenIdCredentialFormatProfileToDifClaimFormat } from './utils' -import { getUniformFormat } from './utils/Formats' +import { getFormatForVersion, getUniformFormat } from './utils/Formats' import { getMetadataFromCredentialOffer, getOfferedCredentialsWithMetadata, @@ -393,6 +393,7 @@ export class OpenId4VcHolderService { const isInlineOffer = credentialWithMetadata.offerType === OfferedCredentialType.InlineCredentialOffer const format = credentialWithMetadata.format + const originalFormat = getFormatForVersion(format, version) let credentialTypes: string | string[] if (version < OpenId4VCIVersion.VER_1_0_11) { @@ -402,7 +403,7 @@ export class OpenId4VcHolderService { `No id provided for a credential supported entry in combination with the OpenId4VCI v8 draft` ) } - credentialTypes = credentialWithMetadata.credentialSupported.id.split(`-${format}`)[0] + credentialTypes = credentialWithMetadata.credentialSupported.id.split(`-${originalFormat}`)[0] } else { if (isInlineOffer) credentialTypes = credentialWithMetadata.inlineCredentialOffer.types else credentialTypes = credentialWithMetadata.credentialSupported.types @@ -412,7 +413,7 @@ export class OpenId4VcHolderService { const credentialResponse = await credentialRequestClient.acquireCredentialsUsingProof({ proofInput, credentialTypes, - format, + format: originalFormat, }) const credential = await this.handleCredentialResponse(agentContext, credentialResponse, { @@ -555,29 +556,29 @@ export class OpenId4VcHolderService { // We just guess that the first one is supported if (issuerSupportedCryptographicSuites === undefined) { signatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms[0] - } - - switch (format) { - case 'jwt_vc_json': - case 'jwt_vc_json-ld': - signatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms.find((signatureAlgorithm) => - issuerSupportedCryptographicSuites?.includes(signatureAlgorithm) - ) - break - case 'ldp_vc': - // We need to find it based on the JSON-LD proof type - signatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms.find((signatureAlgorithm) => { - const JwkClass = getJwkClassFromJwaSignatureAlgorithm(signatureAlgorithm) - if (!JwkClass) return false - - const matchingSuite = signatureSuiteRegistry.getByKeyType(JwkClass.keyType) - if (matchingSuite.length === 0) return false - - return issuerSupportedCryptographicSuites?.includes(matchingSuite[0].proofType) - }) - break - default: - throw new AriesFrameworkError(`Unsupported credential format. Requested format '${format}'`) + } else { + switch (format) { + case 'jwt_vc_json': + case 'jwt_vc_json-ld': + signatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms.find((signatureAlgorithm) => + issuerSupportedCryptographicSuites.includes(signatureAlgorithm) + ) + break + case 'ldp_vc': + // We need to find it based on the JSON-LD proof type + signatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms.find((signatureAlgorithm) => { + const JwkClass = getJwkClassFromJwaSignatureAlgorithm(signatureAlgorithm) + if (!JwkClass) return false + + const matchingSuite = signatureSuiteRegistry.getByKeyType(JwkClass.keyType) + if (matchingSuite.length === 0) return false + + return issuerSupportedCryptographicSuites.includes(matchingSuite[0].proofType) + }) + break + default: + throw new AriesFrameworkError(`Unsupported credential format. Requested format '${format}'`) + } } if (!signatureAlgorithm) { diff --git a/packages/openid4vc-holder/tests/fixtures.ts b/packages/openid4vc-holder/tests/fixtures.ts index d900c1d4c2..d5007b52ce 100644 --- a/packages/openid4vc-holder/tests/fixtures.ts +++ b/packages/openid4vc-holder/tests/fixtures.ts @@ -547,3 +547,64 @@ export const waltIdJffJwt_draft_11 = { jsonLdCredentialResponse: jsonLdPermanentResidentCardCredentialResponse, } + +export const waltIssuerPortalV11 = { + issuerMetadata: { + issuer: 'https://issuer.portal.walt.id', + authorization_endpoint: 'https://issuer.portal.walt.id/authorize', + pushed_authorization_request_endpoint: 'https://issuer.portal.walt.id/par', + token_endpoint: 'https://issuer.portal.walt.id/token', + jwks_uri: 'https://issuer.portal.walt.id/jwks', + scopes_supported: ['openid'], + response_modes_supported: ['query', 'fragment'], + grant_types_supported: ['authorization_code', 'urn:ietf:params:oauth:grant-type:pre-authorized_code'], + subject_types_supported: ['public'], + credential_issuer: 'https://issuer.portal.walt.id/.well-known/openid-credential-issuer', + credential_endpoint: 'https://issuer.portal.walt.id/credential', + credentials_supported: [ + { + format: 'jwt_vc_json', + id: 'VerifiableId', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + types: ['VerifiableCredential', 'VerifiableId'], + }, + { + format: 'jwt_vc_json', + id: 'VerifiableDiploma', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableDiploma'], + }, + { + format: 'jwt_vc_json', + id: 'OpenBadgeCredential', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + types: ['VerifiableCredential', 'OpenBadgeCredential'], + }, + ], + batch_credential_endpoint: 'https://issuer.portal.walt.id/batch_credential', + deferred_credential_endpoint: 'https://issuer.portal.walt.id/credential_deferred', + }, + par: { + request_uri: 'urn:ietf:params:oauth:request_uri:b0e16785-d722-42a5-a04f-4beab28e03ea', + expires_in: 'PT4M0.516515278S', + }, + + acquireAccessTokenResponse: { + access_token: '8bb45fb4-3475-49c2-85c7-0b91f687da44', + refresh_token: 'WEjORX8NZccRGtRN4yvXFdYE8MeAOaLLmmGlcRbutq4', + c_nonce: 'cbad6376-f882-44c5-ae88-19bccc0de124', + id_token: + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI4YmI0NWZiNC0zNDc1LTQ5YzItODVjNy0wYjkxZjY4N2RhNDQifQ.Mca0Ln1AvNlxBJftYc1PZKQBlGdBmrHsFRQSBDoCgD0', + token_type: 'Bearer', + expires_in: 300, + }, + + credentialResponse: { + credential: + 'eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCMwIiwidHlwIjoiSldUIiwiYWxnIjoiRWREU0EifQ.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCIsInN1YiI6ImRpZDprZXk6ekRuYWVpcFdnOURNWFB0OWpjbUFCcWFZUlZLYzE5dFgxeGZCUldGc0pTUG9VZE1udiIsIm5iZiI6MTY4NTM1MDc4OSwiaWF0IjoxNjg1MzUwNzg5LCJ2YyI6eyJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVmVyaWZpYWJsZUF0dGVzdGF0aW9uIiwiVmVyaWZpYWJsZUlkIl0sIkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sImlkIjoidXJuOnV1aWQ6NTljZTRhYzItZWM2NS00YjhmLThmOTYtZWE3ODUxMmRmOWQzIiwiaXNzdWVyIjoiZGlkOmp3azpleUpyZEhraU9pSlBTMUFpTENKMWMyVWlPaUp6YVdjaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWEybGtJam9pTjJRMlkySm1NalE0T1dJek5ESTNObUl4TnpJeE9UQTFORGxrTWpNNU1UZ2lMQ0o0SWpvaVJtNUZWVlZoZFdSdE9UbE9NekJpT0RCcWN6aFdkRFJCYms5NGRsSjNXSFJuVW1OTGNUTm5Ra2wxT0NJc0ltRnNaeUk2SWtWa1JGTkJJbjAiLCJpc3N1YW5jZURhdGUiOiIyMDIzLTA1LTI5VDA4OjU5OjQ5WiIsImlzc3VlZCI6IjIwMjMtMDUtMjlUMDg6NTk6NDlaIiwidmFsaWRGcm9tIjoiMjAyMy0wNS0yOVQwODo1OTo0OVoiLCJjcmVkZW50aWFsU2NoZW1hIjp7ImlkIjoiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3dhbHQtaWQvd2FsdGlkLXNzaWtpdC12Y2xpYi9tYXN0ZXIvc3JjL3Rlc3QvcmVzb3VyY2VzL3NjaGVtYXMvVmVyaWZpYWJsZUlkLmpzb24iLCJ0eXBlIjoiRnVsbEpzb25TY2hlbWFWYWxpZGF0b3IyMDIxIn0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6RG5hZWlwV2c5RE1YUHQ5amNtQUJxYVlSVktjMTl0WDF4ZkJSV0ZzSlNQb1VkTW52IiwiY3VycmVudEFkZHJlc3MiOlsiMSBCb3VsZXZhcmQgZGUgbGEgTGliZXJ0w6ksIDU5ODAwIExpbGxlIl0sImRhdGVPZkJpcnRoIjoiMTk5My0wNC0wOCIsImZhbWlseU5hbWUiOiJET0UiLCJmaXJzdE5hbWUiOiJKYW5lIiwiZ2VuZGVyIjoiRkVNQUxFIiwibmFtZUFuZEZhbWlseU5hbWVBdEJpcnRoIjoiSmFuZSBET0UiLCJwZXJzb25hbElkZW50aWZpZXIiOiIwOTA0MDA4MDg0SCIsInBsYWNlT2ZCaXJ0aCI6IkxJTExFLCBGUkFOQ0UifSwiZXZpZGVuY2UiOlt7ImRvY3VtZW50UHJlc2VuY2UiOlsiUGh5c2ljYWwiXSwiZXZpZGVuY2VEb2N1bWVudCI6WyJQYXNzcG9ydCJdLCJzdWJqZWN0UHJlc2VuY2UiOiJQaHlzaWNhbCIsInR5cGUiOlsiRG9jdW1lbnRWZXJpZmljYXRpb24iXSwidmVyaWZpZXIiOiJkaWQ6ZWJzaToyQTlCWjlTVWU2QmF0YWNTcHZzMVY1Q2RqSHZMcFE3YkVzaTJKYjZMZEhLblF4YU4ifV19LCJqdGkiOiJ1cm46dXVpZDo1OWNlNGFjMi1lYzY1LTRiOGYtOGY5Ni1lYTc4NTEyZGY5ZDMifQ.6Wn8X2tEQJ9CmX3-meCxDuGmevRdtivnjVkGPXzfnJ-1M6AU4SFxxon0JmMjdmO_h4P9sCEe9RTtyTJou2yeCA', + format: 'jwt_vc', + }, +} diff --git a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts index bbf8d6e6f2..918a63c735 100644 --- a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts @@ -22,6 +22,7 @@ import { // mattrLaunchpadJsonLd_draft_08, waltIdJffJwt_draft_08, waltIdJffJwt_draft_11, + waltIssuerPortalV11, } from './fixtures' const modules = { @@ -37,10 +38,10 @@ describe('OpenId4VcHolder', () => { beforeEach(async () => { agent = new Agent({ config: { - label: 'OpenId4VcHolder Test13', + label: 'OpenId4VcHolder Test20', walletConfig: { - id: 'openid4vc-holder-test14', - key: 'openid4vc-holder-test15', + id: 'openid4vc-holder-test21', + key: 'openid4vc-holder-test22', }, }, dependencies: agentDependencies, @@ -259,18 +260,27 @@ describe('OpenId4VcHolder', () => { const clientId = 'test-client' const redirectUri = 'https://example.com/cb' - const resolved = await agent.modules.openId4VcHolder.resolveCredentialOffer( + const resolvedOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer( fixture.credentialOfferAuthorizationCodeFlow ) + const resolvedAuthRequest = await agent.modules.openId4VcHolder.resolveAuthorizationRequest(resolvedOffer, { + clientId, + redirectUri, + scope: ['openid'], + }) + await expect( - agent.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode(resolved, { - clientId: clientId, - verifyCredentialStatus: false, - proofOfPossessionVerificationMethodResolver: () => verificationMethod, - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - redirectUri: redirectUri, - }) + agent.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( + resolvedOffer, + resolvedAuthRequest, + 'code', + { + verifyCredentialStatus: false, + proofOfPossessionVerificationMethodResolver: () => verificationMethod, + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + } + ) ).rejects.toThrow() }) @@ -327,23 +337,26 @@ describe('OpenId4VcHolder', () => { const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) if (!verificationMethod) throw new Error('No verification method found') - const clientId = 'test-client' - const redirectUri = 'https://example.com/cb' - const scope = ['TestCredential'] + const opts = { + clientId: 'test-client', + redirectUri: 'https://example.com/cb', + scope: ['TestCredential'], + } - const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer( + const resolvedOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer( fixture.credentialOfferAuthorizationCodeFlow ) + const resolvedAuthRequest = await agent.modules.openId4VcHolder.resolveAuthorizationRequest(resolvedOffer, opts) + const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( - resolvedCredentialOffer, + resolvedOffer, + resolvedAuthRequest, + 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJiMGUxNjc4NS1kNzIyLTQyYTUtYTA0Zi00YmVhYjI4ZTAzZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.ibEpHFaHFBLWyhEf4SotDQZBeh_FMrfncWapNox1Iv1kdQWQ2cLQeS1VrCyVmPsbx0tN2MAyDFG7DnAaq8MiAA', { - clientId: clientId, verifyCredentialStatus: false, proofOfPossessionVerificationMethodResolver: () => verificationMethod, allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - redirectUri: redirectUri, - scope, } ) @@ -582,32 +595,69 @@ describe('OpenId4VcHolder', () => { expect(w3cCredentialRecord1.credential.credentialSubjectIds[0]).toEqual(did.didState.did) }) - //it('use with jff / mattr demo', async () => { - // const did = await agent.dids.create({ - // method: 'key', - // options: { keyType: KeyType.Ed25519 }, - // secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, - // }) - - // const credentialOffer = `openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.portal.walt.id%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22VerifiableAttestation%22%2C%22VerifiableDiploma%22%5D%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fw3id.org%2Fsecurity%2Fsuites%2Fjws-2020%2Fv1%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22VerifiableAttestation%22%2C%22VerifiableDiploma%22%5D%7D%7D%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22dd8c7950-3f4e-4c61-8b40-4be18c980e46%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJkZDhjNzk1MC0zZjRlLTRjNjEtOGI0MC00YmUxOGM5ODBlNDYiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.UB8riE2_SNxRE_0jXStlpwDrkusmNnCQZgBAGW74xmi3BKPQgnqIB4m_MTHKjA9KhVitKjCoWH8iJdD7nQDVDQ%22%2C%22user_pin_required%22%3Afalse%7D%7D%7D` - - // const didKey = DidKey.fromDid(did.didState.did as string) - // const kid = `${didKey.did}#${didKey.key.fingerprint}` - // const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) - // if (!verificationMethod) throw new Error('No verification method found') - // const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) - - // const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( - // resolvedCredentialOffer, - // { - // allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - // proofOfPossessionVerificationMethodResolver: () => verificationMethod, - // verifyCredentialStatus: false, - // clientId: 'https://issuer.portal.walt.id', - // redirectUri: 'https://example.com/cb', - // scope: ['openid'], - // } - // ) - //}) + it('authorization code flow https://portal.walt.id/', async () => { + const fixture = waltIssuerPortalV11 + // setup temporary redirect mock + nock('https://issuer.portal.walt.id') + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.issuerMetadata) + .get('/.well-known/openid-configuration') + .reply(404) + .get('/.well-known/oauth-authorization-server') + .reply(404) + .post('/par') + .reply(200, fixture.par) + // setup access token response + .post('/token') + .reply(200, fixture.acquireAccessTokenResponse) + // setup credential request response + .post('/credential') + .reply(200, fixture.credentialResponse) + + .get('/.well-known/oauth-authorization-server') + .reply(404) + + const did = await agent.dids.create({ + method: 'key', + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, + }) + + const credentialOffer = `openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.portal.walt.id%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22OpenBadgeCredential%22%5D%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fpurl.imsglobal.org%2Fspec%2Fob%2Fv3p0%2Fcontext.json%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22OpenBadgeCredential%22%5D%7D%7D%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22b0e16785-d722-42a5-a04f-4beab28e03ea%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJiMGUxNjc4NS1kNzIyLTQyYTUtYTA0Zi00YmVhYjI4ZTAzZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.ibEpHFaHFBLWyhEf4SotDQZBeh_FMrfncWapNox1Iv1kdQWQ2cLQeS1VrCyVmPsbx0tN2MAyDFG7DnAaq8MiAA%22%2C%22user_pin_required%22%3Afalse%7D%7D%7D` + + const didKey = DidKey.fromDid(did.didState.did as string) + const kid = `${didKey.did}#${didKey.key.fingerprint}` + const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) + if (!verificationMethod) throw new Error('No verification method found') + + const resolved = await agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) + + const authCodeFlowOptions = { + clientId: 'test-client', + redirectUri: 'http://blank', + scope: ['openid', 'OpenBadgeCredential'], + } + + const resolvedAuthorizationRequest = await agent.modules.openId4VcHolder.resolveAuthorizationRequest( + resolved, + authCodeFlowOptions + ) + + const code = + 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJiMGUxNjc4NS1kNzIyLTQyYTUtYTA0Zi00YmVhYjI4ZTAzZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.ibEpHFaHFBLWyhEf4SotDQZBeh_FMrfncWapNox1Iv1kdQWQ2cLQeS1VrCyVmPsbx0tN2MAyDFG7DnAaq8MiAA' + + const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( + resolved, + resolvedAuthorizationRequest, + code, + { + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + proofOfPossessionVerificationMethodResolver: () => verificationMethod, + verifyCredentialStatus: false, + } + ) + + expect(w3cCredentialRecords).toHaveLength(1) + }) }) }) From 082934bd40612a07db20a0afc7ad6e2ef6c61dae Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Thu, 26 Oct 2023 14:18:22 +0200 Subject: [PATCH 026/115] refactor: issuermetadata Signed-off-by: Martin Auer --- .../src/issuance/utils/IssuerMetadataUtils.ts | 30 +++++-------------- .../tests/openid4vci-holder.e2e.test.ts | 12 +++----- 2 files changed, 12 insertions(+), 30 deletions(-) diff --git a/packages/openid4vc-holder/src/issuance/utils/IssuerMetadataUtils.ts b/packages/openid4vc-holder/src/issuance/utils/IssuerMetadataUtils.ts index 8fb1ae4065..ae13032abd 100644 --- a/packages/openid4vc-holder/src/issuance/utils/IssuerMetadataUtils.ts +++ b/packages/openid4vc-holder/src/issuance/utils/IssuerMetadataUtils.ts @@ -75,8 +75,6 @@ export function getOfferedCredentialsWithMetadata( ) } - // TODO: use getUniFormat?? - for (const foundSupportedCredential of foundSupportedCredentials) { offeredCredentials.push({ credentialSupported: foundSupportedCredential, @@ -187,31 +185,19 @@ export function handleAuthorizationDetails( authorizationDetails: AuthDetails | AuthDetails[], metadata: EndpointMetadataResult ): AuthDetails | AuthDetails[] | undefined { - if (authorizationDetails) { - if (Array.isArray(authorizationDetails)) { - return authorizationDetails.map((value) => handleLocations({ ...value }, metadata)) - } else { - return handleLocations({ ...authorizationDetails }, metadata) - } + if (Array.isArray(authorizationDetails)) { + return authorizationDetails.map((value) => handleLocations({ ...value }, metadata)) + } else { + return handleLocations({ ...authorizationDetails }, metadata) } - return authorizationDetails } // copied from sphereon export function handleLocations(authorizationDetails: AuthDetails, metadata: EndpointMetadataResult) { - if ( - authorizationDetails && - (metadata.credentialIssuerMetadata?.authorization_server || metadata.authorization_endpoint) - ) { - if (authorizationDetails.locations) { - if (Array.isArray(authorizationDetails.locations)) { - ;(authorizationDetails.locations as string[]).push(metadata.issuer) - } else { - authorizationDetails.locations = [authorizationDetails.locations as string, metadata.issuer] - } - } else { - authorizationDetails.locations = metadata.issuer - } + if (metadata.credentialIssuerMetadata?.authorization_server || metadata.authorization_endpoint) { + if (!authorizationDetails.locations) authorizationDetails.locations = metadata.issuer + else if (Array.isArray(authorizationDetails.locations)) authorizationDetails.locations.push(metadata.issuer) + else authorizationDetails.locations = [authorizationDetails.locations as string, metadata.issuer] } return authorizationDetails } diff --git a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts index 918a63c735..c8ddb91b77 100644 --- a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts @@ -14,7 +14,8 @@ import { agentDependencies } from '@aries-framework/node' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import nock, { cleanAll, enableNetConnect } from 'nock' -import { OpenId4VcHolderModule, OpenIdCredentialFormatProfile } from '../src' +import { OpenIdCredentialFormatProfile } from '../src' +import { OpenId4VcHolderModule } from '../src/issuance/OpenId4VciHolderModule' import { mattrLaunchpadJsonLd_draft_08, @@ -632,16 +633,11 @@ describe('OpenId4VcHolder', () => { const resolved = await agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) - const authCodeFlowOptions = { + const resolvedAuthorizationRequest = await agent.modules.openId4VcHolder.resolveAuthorizationRequest(resolved, { clientId: 'test-client', redirectUri: 'http://blank', scope: ['openid', 'OpenBadgeCredential'], - } - - const resolvedAuthorizationRequest = await agent.modules.openId4VcHolder.resolveAuthorizationRequest( - resolved, - authCodeFlowOptions - ) + }) const code = 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJiMGUxNjc4NS1kNzIyLTQyYTUtYTA0Zi00YmVhYjI4ZTAzZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.ibEpHFaHFBLWyhEf4SotDQZBeh_FMrfncWapNox1Iv1kdQWQ2cLQeS1VrCyVmPsbx0tN2MAyDFG7DnAaq8MiAA' From 3fcdd8ba5c549e4914e9c32f5af943c55d6a4a9f Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Thu, 26 Oct 2023 14:31:16 +0200 Subject: [PATCH 027/115] refactor: restructure Signed-off-by: Martin Auer --- .../OpenId4VciHolderApi.ts => OpenId4VcHolderApi.ts} | 6 +++--- ...erviceOptions.ts => OpenId4VcHolderServiceOptions.ts} | 4 ++-- packages/openid4vc-holder/src/index.ts | 4 ++-- .../src/issuance/OpenId4VciHolderModule.ts | 2 +- .../src/issuance/OpenId4VciHolderService.ts | 9 +++++---- .../src/issuance/utils/IssuerMetadataUtils.ts | 2 +- .../openid4vc-holder/tests/OpenId4VcHolderModule.test.ts | 2 +- 7 files changed, 15 insertions(+), 14 deletions(-) rename packages/openid4vc-holder/src/{issuance/OpenId4VciHolderApi.ts => OpenId4VcHolderApi.ts} (95%) rename packages/openid4vc-holder/src/{issuance/OpenId4VciHolderServiceOptions.ts => OpenId4VcHolderServiceOptions.ts} (97%) diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderApi.ts b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts similarity index 95% rename from packages/openid4vc-holder/src/issuance/OpenId4VciHolderApi.ts rename to packages/openid4vc-holder/src/OpenId4VcHolderApi.ts index cf22dc010e..8c9d7e0340 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderApi.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts @@ -3,16 +3,16 @@ import type { AuthCodeFlowOptions, AcceptCredentialOfferOptions, ResolvedAuthorizationRequest, -} from './OpenId4VciHolderServiceOptions' +} from './OpenId4VcHolderServiceOptions' import type { VerificationMethod, W3cCredentialRecord } from '@aries-framework/core' import type { VerifiedAuthorizationRequest } from '@sphereon/did-auth-siop' import type { CredentialOfferPayloadV1_0_11 } from '@sphereon/oid4vci-common' import { injectable, AgentContext } from '@aries-framework/core' -import { OpenId4VpHolderService } from '../presentations/OpenId4VpHolderService' +import { OpenId4VpHolderService } from './presentations/OpenId4VpHolderService' -import { OpenId4VcHolderService } from './OpenId4VciHolderService' +import { OpenId4VcHolderService } from './issuance/OpenId4VciHolderService' /** * @public diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderServiceOptions.ts b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts similarity index 97% rename from packages/openid4vc-holder/src/issuance/OpenId4VciHolderServiceOptions.ts rename to packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts index c7d58528b9..3d26fb4e69 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderServiceOptions.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts @@ -1,9 +1,9 @@ -import type { OfferedCredentialType } from './utils/IssuerMetadataUtils' +import type { OfferedCredentialType } from './issuance/utils/IssuerMetadataUtils' import type { JwaSignatureAlgorithm, KeyType, VerificationMethod } from '@aries-framework/core' import type { CredentialOfferPayloadV1_0_11, EndpointMetadataResult, OpenId4VCIVersion } from '@sphereon/oid4vci-common' import type { CredentialFormat } from '@sphereon/ssi-types' -import { OpenIdCredentialFormatProfile } from './utils/claimFormatMapping' +import { OpenIdCredentialFormatProfile } from './issuance/utils/claimFormatMapping' // TODO: use simpler object export interface AuthDetails { diff --git a/packages/openid4vc-holder/src/index.ts b/packages/openid4vc-holder/src/index.ts index 8234727681..70e324fce5 100644 --- a/packages/openid4vc-holder/src/index.ts +++ b/packages/openid4vc-holder/src/index.ts @@ -1,6 +1,6 @@ import 'fast-text-encoding' -export * from './issuance/OpenId4VciHolderApi' +export * from './OpenId4VcHolderApi' export * from './issuance/OpenId4VciHolderModule' export * from './issuance/OpenId4VciHolderService' // Contains internal types, so we don't export everything @@ -11,6 +11,6 @@ export { ProofOfPossessionVerificationMethodResolverOptions, RequestCredentialOptions, SupportedCredentialFormats, -} from './issuance/OpenId4VciHolderServiceOptions' +} from './OpenId4VcHolderServiceOptions' export * from './presentations' export { OpenIdCredentialFormatProfile } from './issuance/utils' diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderModule.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderModule.ts index 1a09cdb8dc..ee44b81b28 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderModule.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderModule.ts @@ -2,10 +2,10 @@ import type { DependencyManager, Module } from '@aries-framework/core' import { AgentConfig } from '@aries-framework/core' +import { OpenId4VcHolderApi } from '../OpenId4VcHolderApi' import { PresentationExchangeService } from '../presentations' import { OpenId4VpHolderService } from '../presentations/OpenId4VpHolderService' -import { OpenId4VcHolderApi } from './OpenId4VciHolderApi' import { OpenId4VcHolderService } from './OpenId4VciHolderService' /** diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts index 904863f5a9..cb4a277813 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts @@ -1,3 +1,5 @@ +import type { OpenIdCredentialFormatProfile } from './utils' +import type { OfferedCredentialWithMetadata } from './utils/IssuerMetadataUtils' import type { AuthCodeFlowOptions, AuthDetails, @@ -8,9 +10,7 @@ import type { SupportedCredentialFormats, ResolvedCredentialOffer, ResolvedAuthorizationRequest, -} from './OpenId4VciHolderServiceOptions' -import type { OpenIdCredentialFormatProfile } from './utils' -import type { OfferedCredentialWithMetadata } from './utils/IssuerMetadataUtils' +} from '../OpenId4VcHolderServiceOptions' import type { AgentContext, JwaSignatureAlgorithm, @@ -71,7 +71,8 @@ import { } from '@sphereon/oid4vci-common' import { randomStringForEntropy } from '@stablelib/random' -import { supportedCredentialFormats } from './OpenId4VciHolderServiceOptions' +import { supportedCredentialFormats } from '../OpenId4VcHolderServiceOptions' + import { fromOpenIdCredentialFormatProfileToDifClaimFormat } from './utils' import { getFormatForVersion, getUniformFormat } from './utils/Formats' import { diff --git a/packages/openid4vc-holder/src/issuance/utils/IssuerMetadataUtils.ts b/packages/openid4vc-holder/src/issuance/utils/IssuerMetadataUtils.ts index ae13032abd..7af1670c50 100644 --- a/packages/openid4vc-holder/src/issuance/utils/IssuerMetadataUtils.ts +++ b/packages/openid4vc-holder/src/issuance/utils/IssuerMetadataUtils.ts @@ -1,4 +1,4 @@ -import type { AuthDetails } from '../OpenId4VciHolderServiceOptions' +import type { AuthDetails } from '../../OpenId4VcHolderServiceOptions' import type { CredentialIssuerMetadata, CredentialOfferFormat, diff --git a/packages/openid4vc-holder/tests/OpenId4VcHolderModule.test.ts b/packages/openid4vc-holder/tests/OpenId4VcHolderModule.test.ts index b2fdd907cc..f138572705 100644 --- a/packages/openid4vc-holder/tests/OpenId4VcHolderModule.test.ts +++ b/packages/openid4vc-holder/tests/OpenId4VcHolderModule.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/unbound-method */ import type { DependencyManager } from '@aries-framework/core' -import { OpenId4VcHolderApi } from '../src/issuance/OpenId4VciHolderApi' +import { OpenId4VcHolderApi } from '../src/OpenId4VcHolderApi' import { OpenId4VcHolderModule } from '../src/issuance/OpenId4VciHolderModule' import { OpenId4VcHolderService } from '../src/issuance/OpenId4VciHolderService' import { OpenId4VpHolderService, PresentationExchangeService } from '../src/presentations' From 42d18010e2d6e4a6d83c8647c5d456334b3c6683 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 31 Oct 2023 16:55:40 +0100 Subject: [PATCH 028/115] fix: incorporate parts of the pr feedback Signed-off-by: Martin Auer --- packages/core/package.json | 1 - packages/core/src/index.ts | 1 + packages/core/src/utils/deepEquality.ts | 2 +- packages/openid4vc-holder/package.json | 8 +- .../src/OpenId4VcHolderApi.ts | 59 ++- .../src/OpenId4VcHolderServiceOptions.ts | 1 - packages/openid4vc-holder/src/index.ts | 2 - .../src/issuance/OpenId4VciHolderService.ts | 168 +++++-- .../src/issuance/utils/Formats.ts | 2 +- .../src/issuance/utils/IssuerMetadataUtils.ts | 69 ++- .../presentations/OpenId4VpHolderService.ts | 432 +++++++++++++++--- .../PresentationExchangeService.ts | 8 +- .../selection/PexCredentialSelection.ts | 1 + .../src/presentations/transform.ts | 2 - .../tests/openid4vci-holder.e2e.test.ts | 38 ++ .../tests/openid4vp-holder.e2e.test.ts | 209 +++++++-- packages/openid4vc-issuer/package.json | 4 +- packages/openid4vc-issuer/src/index.ts | 2 - .../tests/openid4vc-issuer.e2e.test.ts | 15 +- packages/openid4vc-verifier/package.json | 4 +- packages/openid4vc-verifier/src/index.ts | 2 - .../tests/openid4vc-verifier.e2e.test.ts | 15 +- yarn.lock | 55 ++- 23 files changed, 824 insertions(+), 276 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 207913feeb..c213dc3df5 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -28,7 +28,6 @@ "@digitalcredentials/vc": "^1.1.2", "@multiformats/base-x": "^4.0.1", "@stablelib/ed25519": "^1.0.2", - "@stablelib/random": "^1.0.1", "@stablelib/sha256": "^1.0.1", "@types/node-fetch": "2.6.2", "@types/ws": "^8.5.4", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 99406b3d2e..8e521dbd31 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -69,6 +69,7 @@ export { TypedArrayEncoder, Buffer, asArray, + equalsIgnoreOrder, } from './utils' export * from './logger' export * from './error' diff --git a/packages/core/src/utils/deepEquality.ts b/packages/core/src/utils/deepEquality.ts index a8bb286d73..b2f2ac7aad 100644 --- a/packages/core/src/utils/deepEquality.ts +++ b/packages/core/src/utils/deepEquality.ts @@ -26,7 +26,7 @@ export function deepEquality(x: any, y: any): boolean { /** * @note This will only work for primitive array equality */ -function equalsIgnoreOrder(a: Array, b: Array): boolean { +export function equalsIgnoreOrder(a: Array, b: Array): boolean { if (a.length !== b.length) return false return a.every((k) => b.includes(k)) } diff --git a/packages/openid4vc-holder/package.json b/packages/openid4vc-holder/package.json index 3dd359d460..615907b036 100644 --- a/packages/openid4vc-holder/package.json +++ b/packages/openid4vc-holder/package.json @@ -27,13 +27,11 @@ "@aries-framework/core": "0.4.2", "@sphereon/oid4vci-client": "^0.8.1", "@sphereon/oid4vci-common": "^0.8.1", - "@sphereon/did-auth-siop": "^0.4.2", - "@sphereon/pex": "^2.1.3-unstable.6", + "@sphereon/did-auth-siop": "^0.5.0-unstable.7", + "@sphereon/pex": "^2.2.1-unstable.0", "@sphereon/pex-models": "^2.1.1", "jsonpath": "1.1.1", - "@sphereon/ssi-types": "^0.17.5", - "@stablelib/random": "^1.0.2", - "fast-text-encoding": "^1.0.6" + "@sphereon/ssi-types": "^0.17.5" }, "devDependencies": { "@aries-framework/askar": "0.4.2", diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts index 8c9d7e0340..2a83335c06 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts @@ -5,14 +5,13 @@ import type { ResolvedAuthorizationRequest, } from './OpenId4VcHolderServiceOptions' import type { VerificationMethod, W3cCredentialRecord } from '@aries-framework/core' -import type { VerifiedAuthorizationRequest } from '@sphereon/did-auth-siop' +import type { ClientMetadataOpts, VerifiedAuthorizationRequest } from '@sphereon/did-auth-siop' import type { CredentialOfferPayloadV1_0_11 } from '@sphereon/oid4vci-common' import { injectable, AgentContext } from '@aries-framework/core' -import { OpenId4VpHolderService } from './presentations/OpenId4VpHolderService' - import { OpenId4VcHolderService } from './issuance/OpenId4VciHolderService' +import { OpenId4VpHolderService } from './presentations/OpenId4VpHolderService' /** * @public @@ -33,9 +32,45 @@ export class OpenId4VcHolderApi { this.openId4VpHolderService = openId4VpHolderService } - public async resolveRequest(uri: string) { - const resolved = await this.openId4VpHolderService.resolveAuthenticationRequest(this.agentContext, uri) - return resolved + public async createRequest(options: { + verificationMethod: VerificationMethod + redirect_url: string + clientMetadata?: ClientMetadataOpts + issuer?: string + }) { + const relyingParty = await this.openId4VpHolderService.getRelyingParty(this.agentContext, options) + + // TODO: generate nonce, state, correlationId + const nonce = 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg' + const state = 'b32f0087fc9816eb813fd11f' + const correlationId = '1' + + const authorizationRequest = await relyingParty.createAuthorizationRequest({ + correlationId, + nonce, + state, + }) + + const authorizationRequestUri = await authorizationRequest.uri() + const encodedAuthorizationRequestUri = authorizationRequestUri.encodedUri + + return { + relyingParty, + authorizationRequestUri: encodedAuthorizationRequestUri, + correlationId, + nonce, + state, + } + } + + /** + * Resolves the authentication request given as URI or JWT to a unified @class VerificationRequest, + * then verifies the validity of the request and return the @class VerifiedAuthorizationRequest. + * @param requestJwtOrUri JWT or an openid:// URI + * @returns the Verified Authorization Request + */ + public async resolveRequest(requestOrJwt: string) { + return await this.openId4VpHolderService.resolveAuthorizationRequest(this.agentContext, requestOrJwt) } public async acceptRequest(verifiedRequest: VerifiedAuthorizationRequest, verificationMethod: VerificationMethod) { @@ -54,14 +89,18 @@ export class OpenId4VcHolderApi { * @returns The uniform credential offer payload, the issuer metadata, protocol version, and credentials that can be requested. */ public async resolveCredentialOffer(credentialOffer: string | CredentialOfferPayloadV1_0_11) { - const resolvedCredentialOffer = await this.openId4VcHolderService.resolveCredentialOffer(credentialOffer) - return resolvedCredentialOffer + return await this.openId4VcHolderService.resolveCredentialOffer(credentialOffer) } /** * This function is to be used with the Authorization Code Flow. * It will generate the authorization request URI based on the provided options. * The authorization request URI is used to obtain the authorization code. Currently this needs to be done manually. + * + * Authorization to request credentials can be requested via authorization_details or scopes. + * This function automatically generates the authorization_details for all offered credentials. + * If scopes are provided, the provided scopes are send alongside the authorization_details. + * * @param resolvedCredentialOffer Obtained through @function resolveCredentialOffer * @param authCodeFlowOptions * @returns The authorization request URI alongside the code verifier and original @param authCodeFlowOptions @@ -70,11 +109,11 @@ export class OpenId4VcHolderApi { resolvedCredentialOffer: ResolvedCredentialOffer, authCodeFlowOptions: AuthCodeFlowOptions ) { - const resolvedAuthorizationRequest = await this.openId4VcHolderService.resolveAuthorizationRequest( + return await this.openId4VcHolderService.resolveAuthorizationRequest( + this.agentContext, resolvedCredentialOffer, authCodeFlowOptions ) - return resolvedAuthorizationRequest } /** diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts index 3d26fb4e69..e3e561af39 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts @@ -94,7 +94,6 @@ export interface AuthCodeFlowOptions { clientId: string redirectUri: string scope?: string[] - authDetails?: AuthDetails[] } export interface ProofOfPossessionVerificationMethodResolverOptions { diff --git a/packages/openid4vc-holder/src/index.ts b/packages/openid4vc-holder/src/index.ts index 70e324fce5..02a938901f 100644 --- a/packages/openid4vc-holder/src/index.ts +++ b/packages/openid4vc-holder/src/index.ts @@ -1,5 +1,3 @@ -import 'fast-text-encoding' - export * from './OpenId4VcHolderApi' export * from './issuance/OpenId4VciHolderModule' export * from './issuance/OpenId4VciHolderService' diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts index cb4a277813..96574abd84 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts @@ -1,4 +1,3 @@ -import type { OpenIdCredentialFormatProfile } from './utils' import type { OfferedCredentialWithMetadata } from './utils/IssuerMetadataUtils' import type { AuthCodeFlowOptions, @@ -53,6 +52,7 @@ import { inject, injectable, parseDid, + equalsIgnoreOrder, } from '@aries-framework/core' import { AccessTokenClient, @@ -69,11 +69,10 @@ import { convertJsonToURI, JsonURIMode, } from '@sphereon/oid4vci-common' -import { randomStringForEntropy } from '@stablelib/random' import { supportedCredentialFormats } from '../OpenId4VcHolderServiceOptions' -import { fromOpenIdCredentialFormatProfileToDifClaimFormat } from './utils' +import { OpenIdCredentialFormatProfile, fromOpenIdCredentialFormatProfileToDifClaimFormat } from './utils' import { getFormatForVersion, getUniformFormat } from './utils/Formats' import { getMetadataFromCredentialOffer, @@ -82,6 +81,26 @@ import { OfferedCredentialType, } from './utils/IssuerMetadataUtils' +function getV8CredentialType( + offeredCredentialWithMetadata: OfferedCredentialWithMetadata, + format: string, + version: OpenId4VCIVersion +) { + if (offeredCredentialWithMetadata.offerType === OfferedCredentialType.InlineCredentialOffer) { + throw new AriesFrameworkError(`Inline credential offers not supported for version < 11`) + } + + if (!offeredCredentialWithMetadata.credentialSupported.id) { + throw new AriesFrameworkError( // This should not happen + `No id provided for a credential supported entry in combination with the OpenId4VCI v8 draft` + ) + } + + const originalFormat = getFormatForVersion(format, version) + const credentialType = offeredCredentialWithMetadata.credentialSupported.id.split(`-${originalFormat}`)[0] + return credentialType +} + async function createAuthorizationRequestUri(options: { credentialOffer: CredentialOfferPayloadV1_0_11 metadata: EndpointMetadataResult @@ -93,22 +112,20 @@ async function createAuthorizationRequestUri(options: { scope?: string[] }) { const { scope, authDetails, metadata, clientId, codeChallenge, codeChallengeMethod, redirectUri } = options - let nonEmptyScope = !scope || scope.length === 0 ? undefined : scope?.join(' ') + let nonEmptyScope = !scope || scope.length === 0 ? undefined : scope const nonEmptyAuthDetails = !authDetails || authDetails.length === 0 ? undefined : authDetails // Scope and authorization_details can be used in the same authorization request // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param if (!nonEmptyScope && !nonEmptyAuthDetails) { - throw new AriesFrameworkError('Please provide a scope or authorization_details') + throw new AriesFrameworkError(`Please provide a 'scope' or 'authDetails' via the options.`) } // Authorization servers supporting PAR SHOULD include the URL of their pushed authorization request endpoint in their authorization server metadata document // Note that the presence of pushed_authorization_request_endpoint is sufficient for a client to determine that it may use the PAR flow. - let parEndpoint = metadata.credentialIssuerMetadata?.pushed_authorization_request_endpoint - if (typeof parEndpoint !== 'string') parEndpoint = undefined + const parEndpoint = metadata.credentialIssuerMetadata?.pushed_authorization_request_endpoint - let authorizationEndpoint = metadata.credentialIssuerMetadata?.authorization_endpoint - if (typeof authorizationEndpoint !== 'string') authorizationEndpoint = undefined + const authorizationEndpoint = metadata.credentialIssuerMetadata?.authorization_endpoint if (!authorizationEndpoint && !parEndpoint) { throw new AriesFrameworkError( @@ -117,11 +134,11 @@ async function createAuthorizationRequestUri(options: { } // add 'openid' scope if not present - if (!nonEmptyScope?.includes('openid')) { - nonEmptyScope = ['openid', nonEmptyScope].filter((s) => !!s).join(' ') + if (nonEmptyScope && !nonEmptyScope?.includes('openid')) { + nonEmptyScope = ['openid', ...nonEmptyScope] } - const queryObj: { [key: string]: string } = { + const queryObj: Record = { client_id: clientId, response_type: ResponseType.AUTH_CODE, code_challenge_method: codeChallengeMethod, @@ -129,7 +146,7 @@ async function createAuthorizationRequestUri(options: { redirect_uri: redirectUri, } - if (nonEmptyScope) queryObj['scope'] = nonEmptyScope + if (nonEmptyScope) queryObj['scope'] = nonEmptyScope.join(' ') if (nonEmptyAuthDetails) queryObj['authorization_details'] = JSON.stringify(handleAuthorizationDetails(nonEmptyAuthDetails, metadata)) @@ -140,8 +157,9 @@ async function createAuthorizationRequestUri(options: { if (parEndpoint) { const body = new URLSearchParams(queryObj) const response = await formPost(parEndpoint, body) - if (!response.successBody) + if (!response.successBody) { throw new AriesFrameworkError(`Could not acquire the authorization request uri from '${parEndpoint}'`) + } return convertJsonToURI( { request_uri: response.successBody.request_uri, client_id: clientId, response_type: ResponseType.AUTH_CODE }, { @@ -235,17 +253,80 @@ export class OpenId4VcHolderService { } } + private getAuthDetailsFromOfferedCredential( + credentialWithMetadata: OfferedCredentialWithMetadata, + authDetailsLocation: string | undefined, + version: OpenId4VCIVersion + ): AuthDetails { + const { format, types } = credentialWithMetadata + const type = 'openid_credential' + + if (version < OpenId4VCIVersion.VER_1_0_11) { + const credential_type = getV8CredentialType(credentialWithMetadata, format, version) + return { type, credential_type, format } + } + + const locations = authDetailsLocation ? [authDetailsLocation] : undefined + if (format === OpenIdCredentialFormatProfile.JwtVcJson || format === OpenIdCredentialFormatProfile.JwtVcJsonLd) { + return { + type, + format, + types, + locations, + } + } else if (format === OpenIdCredentialFormatProfile.LdpVc) { + let context: string | undefined = undefined + + if (credentialWithMetadata.offerType === OfferedCredentialType.InlineCredentialOffer) { + // Inline Credential Offers come with no context + } else { + if ('@context' in credentialWithMetadata.credentialSupported) { + context = credentialWithMetadata.credentialSupported['@context'] as unknown as string + } else { + throw new AriesFrameworkError('Could not find @context in credentialSupported.') + } + } + + return { + type, + format, + types, + locations, + '@context': context, + } + } else { + throw new AriesFrameworkError(`Cannot create authorization_details. Unsupported credential format ${format}.`) + } + } + public async resolveAuthorizationRequest( + agentContext: AgentContext, resolvedCredentialOffer: ResolvedCredentialOffer, authCodeFlowOptions: AuthCodeFlowOptions ): Promise { - const { credentialOfferPayload, metadata: _metadata } = resolvedCredentialOffer - const { metadata } = await getMetadataFromCredentialOffer(credentialOfferPayload, _metadata) - const codeVerifier = randomStringForEntropy(256) + const { credentialOfferPayload, metadata: _metadata, version } = resolvedCredentialOffer + const codeVerifier = ( + await Promise.all([agentContext.wallet.generateNonce(), agentContext.wallet.generateNonce()]) + ).join('') const codeVerifierSha256 = Hasher.hash(TypedArrayEncoder.fromString(codeVerifier), 'sha2-256') const codeChallenge = TypedArrayEncoder.toBase64URL(codeVerifierSha256) - // TODO: authdetails + const { metadata, issuerMetadata } = await getMetadataFromCredentialOffer(credentialOfferPayload, _metadata) + + const offeredCredentialsWithMetadata = getOfferedCredentialsWithMetadata( + credentialOfferPayload, + issuerMetadata, + version + ) + + let authDetailsLocation: string | undefined + if (issuerMetadata.authorization_server) { + authDetailsLocation = metadata.issuer + } + + const authDetails = offeredCredentialsWithMetadata.map((credential) => + this.getAuthDetailsFromOfferedCredential(credential, authDetailsLocation, version) + ) this.logger.debug('Converted code_verifier to code_challenge', { codeVerifier: codeVerifier, @@ -253,7 +334,7 @@ export class OpenId4VcHolderService { base64Url: codeChallenge, }) - const { clientId, redirectUri, scope, authDetails } = authCodeFlowOptions + const { clientId, redirectUri, scope } = authCodeFlowOptions const authorizationRequestUri = await createAuthorizationRequestUri({ credentialOffer: credentialOfferPayload, clientId, @@ -301,7 +382,13 @@ export class OpenId4VcHolderService { : supportedJwaSignatureAlgorithms if (allowedProofOfPossessionSignatureAlgorithms.length === 0) { - throw new AriesFrameworkError(`No supported proof of possession signature algorithm found.`) + throw new AriesFrameworkError( + [ + `No supported proof of possession signature algorithm found.`, + `Signature algorithms supported by the Agent '${supportedJwaSignatureAlgorithms.join(', ')}'`, + `Possible Signature algorithms '${possibleProofOfPossessionSigAlgs?.join(', ')}'`, + ].join('\n') + ) } // acquire the access token @@ -344,12 +431,15 @@ export class OpenId4VcHolderService { const credentialToRequest = offeredCredentialsWithMetadata.find((offeredCredentialWithMetadata) => { const { format, types } = offeredCredentialWithMetadata // only requests credentials with the exact same set of types and format - return ctr.format === format && ctr.types.sort().join(',') === types.sort().join(',') + return ctr.format === format && equalsIgnoreOrder(ctr.types, types) }) if (!credentialToRequest) throw new AriesFrameworkError( - `Could not find the the requested credential with format '${ctr.format}' and types '${ctr.types}' in the offered credentials.` + [ + `Could not find the the requested credential with format '${ctr.format}'`, + `and types '${ctr.types.join()}' in the offered credentials.`, + ].join(' ') ) return credentialToRequest @@ -399,12 +489,7 @@ export class OpenId4VcHolderService { let credentialTypes: string | string[] if (version < OpenId4VCIVersion.VER_1_0_11) { if (isInlineOffer) throw new AriesFrameworkError(`Inline credential offers not supported for version < 11`) - if (!credentialWithMetadata.credentialSupported.id) { - throw new AriesFrameworkError( // This should not happen - `No id provided for a credential supported entry in combination with the OpenId4VCI v8 draft` - ) - } - credentialTypes = credentialWithMetadata.credentialSupported.id.split(`-${originalFormat}`)[0] + credentialTypes = getV8CredentialType(credentialWithMetadata, format, version) } else { if (isInlineOffer) credentialTypes = credentialWithMetadata.inlineCredentialOffer.types else credentialTypes = credentialWithMetadata.credentialSupported.types @@ -528,12 +613,7 @@ export class OpenId4VcHolderService { const credentialSupportedMetadata = isInlineOffer ? undefined : offeredCredentialWithMetadata.credentialSupported const issuerSupportedCryptographicSuites = credentialSupportedMetadata?.cryptographic_suites_supported - const issuerSupportedBindingMethods = - credentialSupportedMetadata?.cryptographic_binding_methods_supported ?? - // FIXME: somehow the MATTR Launchpad returns binding_methods_supported instead of cryptographic_binding_methods_supported - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - (credentialSupportedMetadata?.binding_methods_supported as string[] | undefined) + const issuerSupportedBindingMethods = credentialSupportedMetadata?.cryptographic_binding_methods_supported if (!isInlineOffer) { const credentialMetadata = offeredCredentialWithMetadata.credentialSupported @@ -628,34 +708,32 @@ export class OpenId4VcHolderService { credentialResponse: OpenIDResponse, options: { verifyCredentialStatus: boolean } ) { + const { verifyCredentialStatus } = options this.logger.debug('Credential request response', credentialResponse) if (!credentialResponse.successBody) { throw new AriesFrameworkError('Did not receive a successful credential response') } - const format = getUniformFormat(credentialResponse.successBody.format) - const difClaimFormat = fromOpenIdCredentialFormatProfileToDifClaimFormat(format as OpenIdCredentialFormatProfile) + const format = getUniformFormat(credentialResponse.successBody.format) as OpenIdCredentialFormatProfile + const difClaimFormat = fromOpenIdCredentialFormatProfileToDifClaimFormat(format) let credential: W3cVerifiableCredential let result: W3cVerifyCredentialResult + if (difClaimFormat === ClaimFormat.LdpVc) { + // validate json-ld credentials credential = JsonTransformer.fromJSON(credentialResponse.successBody.credential, W3cJsonLdVerifiableCredential) - result = await this.w3cCredentialService.verifyCredential(agentContext, { - credential, - verifyCredentialStatus: options.verifyCredentialStatus, - }) + result = await this.w3cCredentialService.verifyCredential(agentContext, { credential, verifyCredentialStatus }) } else if (difClaimFormat === ClaimFormat.JwtVc) { + // validate jwt credentials credential = W3cJwtVerifiableCredential.fromSerializedJwt(credentialResponse.successBody.credential as string) - result = await this.w3cCredentialService.verifyCredential(agentContext, { - credential, - verifyCredentialStatus: options.verifyCredentialStatus, - }) + result = await this.w3cCredentialService.verifyCredential(agentContext, { credential, verifyCredentialStatus }) } else { throw new AriesFrameworkError(`Unsupported credential format ${credentialResponse.successBody.format}`) } - if (!result || !result.isValid) { + if (!result.isValid) { agentContext.config.logger.error('Failed to validate credential', { result }) throw new AriesFrameworkError(`Failed to validate credential, error = ${result.error?.message ?? 'Unknown'}`) } diff --git a/packages/openid4vc-holder/src/issuance/utils/Formats.ts b/packages/openid4vc-holder/src/issuance/utils/Formats.ts index f529ad0620..77a8259f9d 100644 --- a/packages/openid4vc-holder/src/issuance/utils/Formats.ts +++ b/packages/openid4vc-holder/src/issuance/utils/Formats.ts @@ -3,7 +3,7 @@ import type { CredentialFormat } from '@sphereon/ssi-types' import { OpenId4VCIVersion } from '@sphereon/oid4vci-common' -// Base on https://github.com/Sphereon-Opensource/OID4VCI/pull/54/files +// Based on https://github.com/Sphereon-Opensource/OID4VCI/pull/54/files const isUniformFormat = (format: string): format is OID4VCICredentialFormat => { return ['jwt_vc_json', 'jwt_vc_json-ld', 'ldp_vc'].includes(format) diff --git a/packages/openid4vc-holder/src/issuance/utils/IssuerMetadataUtils.ts b/packages/openid4vc-holder/src/issuance/utils/IssuerMetadataUtils.ts index 7af1670c50..ff35e8372d 100644 --- a/packages/openid4vc-holder/src/issuance/utils/IssuerMetadataUtils.ts +++ b/packages/openid4vc-holder/src/issuance/utils/IssuerMetadataUtils.ts @@ -8,7 +8,6 @@ import type { CredentialSupportedV1_0_08, EndpointMetadataResult, IssuerMetadataV1_0_08, - MetadataDisplay, OID4VCICredentialFormat, } from '@sphereon/oid4vci-common' @@ -100,30 +99,31 @@ export function getOfferedCredentialsWithMetadata( export async function getMetadataFromCredentialOffer( credentialOfferPayload: CredentialOfferPayloadV1_0_11, - _metadata?: EndpointMetadataResult + metadata?: EndpointMetadataResult ) { const issuer = credentialOfferPayload.credential_issuer - const metadata = - _metadata && _metadata.credentialIssuerMetadata ? _metadata : await MetadataClient.retrieveAllMetadata(issuer) - if (!metadata) throw new AriesFrameworkError(`Could not retrieve metadata for OpenId4Vci issuer: ${issuer}`) + const resolvedMetadata = + metadata && metadata.credentialIssuerMetadata ? metadata : await MetadataClient.retrieveAllMetadata(issuer) - const issuerMetadata = metadata.credentialIssuerMetadata - if (!issuerMetadata) + if (!resolvedMetadata) { + throw new AriesFrameworkError(`Could not retrieve metadata for OpenId4Vci issuer: ${issuer}`) + } + + const issuerMetadata = resolvedMetadata.credentialIssuerMetadata + if (!issuerMetadata) { throw new AriesFrameworkError(`Could not retrieve issuer metadata for OpenId4Vci issuer: ${issuer}`) + } - return { issuer, metadata, issuerMetadata } + return { issuer, metadata: resolvedMetadata, issuerMetadata } } -export function getSupportedCredentials(opts?: { +export function getSupportedCredentials(opts: { issuerMetadata: CredentialIssuerMetadata | IssuerMetadataV1_0_08 version: OpenId4VCIVersion }): CredentialSupported[] { - const { issuerMetadata } = opts ?? {} + const { issuerMetadata } = opts let credentialsSupported: CredentialSupported[] - if (!issuerMetadata) { - return [] - } const { version } = opts ?? { version: OpenId4VCIVersion.VER_1_0_11 } const usesTransformedCredentialsSupported = @@ -161,7 +161,6 @@ export function credentialSupportedV8ToV11( if (typeof format !== 'string') { throw Error(`Unknown format received ${JSON.stringify(format)}`) } - let credentialSupport: Partial = {} // v8 format included the credential type / id as the key of the object and it could contain multiple supported formats // v11 format has an array where each entry only supports one format, and can only have an `id` property. We include the @@ -169,14 +168,26 @@ export function credentialSupportedV8ToV11( // one key), we append the format to the key IF there's more than one format supported under the key. const id = v8FormatEntries.length > 1 ? `${key}-${format}` : key - credentialSupport = { - format, - display: supportedV8.display, - ...credentialSupportBrief, - credentialSubject: supportedV8.claims, - id, + let credentialSupported: CredentialSupported + if (format === 'jwt_vc_json') { + credentialSupported = { + format, + display: supportedV8.display, + ...credentialSupportBrief, + credentialSubject: supportedV8.claims, + id, + } + } else { + credentialSupported = { + format, + display: supportedV8.display, + ...credentialSupportBrief, + id, + '@context': ['VerifiableCredential'], // NOTE: V8 credentials don't come with @context + } } - return credentialSupport as CredentialSupported + + return credentialSupported }) } @@ -201,19 +212,3 @@ export function handleLocations(authorizationDetails: AuthDetails, metadata: End } return authorizationDetails } - -// TODO -export function getIssuerDisplays( - metadata: CredentialIssuerMetadata | IssuerMetadataV1_0_08, - opts?: { prefLocales: string[] } -): MetadataDisplay[] { - const matchedDisplays = - metadata.display?.filter( - (item) => - !opts?.prefLocales || - opts.prefLocales.length === 0 || - (item.locale && opts.prefLocales.includes(item.locale)) || - !item.locale - ) ?? [] - return matchedDisplays.sort((item) => (item.locale ? opts?.prefLocales.indexOf(item.locale) ?? 1 : Number.MAX_VALUE)) -} diff --git a/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts b/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts index c1368a9021..0184929b1e 100644 --- a/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts +++ b/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts @@ -1,10 +1,17 @@ import type { PresentationSubmission } from './selection' import type { CredentialsForInputDescriptor } from './selection/types' -import type { AgentContext, W3cCredentialRecord, W3cVerifiablePresentation } from '@aries-framework/core' import type { + AgentContext, + JwaSignatureAlgorithm, + VerificationMethod, + W3cCredentialRecord, + W3cVerifiablePresentation, +} from '@aries-framework/core' +import type { + ClientMetadataOpts, DIDDocument, PresentationDefinitionWithLocation, - SigningAlgo, + PresentationVerificationCallback, URI, Verification, VerifiedAuthorizationRequest, @@ -14,7 +21,6 @@ import type { W3CVerifiablePresentation } from '@sphereon/ssi-types' import { AriesFrameworkError, - Buffer, DidsApi, getJwkClassFromKeyType, getKeyFromVerificationMethod, @@ -22,11 +28,63 @@ import { TypedArrayEncoder, W3cJsonLdVerifiablePresentation, asArray, + inject, + InjectionSymbols, + Logger, + JwsService, } from '@aries-framework/core' -import { CheckLinkedDomain, OP, ResponseMode, SupportedVersion, VerificationMode } from '@sphereon/did-auth-siop' +import { + OP, + ResponseIss, + ResponseMode, + SupportedVersion, + VerificationMode, + CheckLinkedDomain, + RP, + SigningAlgo, + RevocationVerification, + ResponseType, + Scope, + SubjectType, + PassBy, +} from '@sphereon/did-auth-siop' import { PresentationExchangeService } from './PresentationExchangeService' +export const staticOpSiopConfig: ClientMetadataOpts & { authorization_endpoint: string } = { + authorization_endpoint: 'siopv2:', + subject_syntax_types_supported: ['urn:ietf:params:oauth:jwk-thumbprint'], + responseTypesSupported: [ResponseType.ID_TOKEN], + scopesSupported: [Scope.OPENID], + subjectTypesSupported: [SubjectType.PAIRWISE], + idTokenSigningAlgValuesSupported: [SigningAlgo.ES256], + requestObjectSigningAlgValuesSupported: [SigningAlgo.ES256], + passBy: PassBy.VALUE, +} + +export const staticOpOpenIdConfig: ClientMetadataOpts & { authorization_endpoint: string } = { + authorization_endpoint: 'openid:', + subject_syntax_types_supported: ['urn:ietf:params:oauth:jwk-thumbprint'], + responseTypesSupported: [ResponseType.ID_TOKEN, ResponseType.VP_TOKEN], + scopesSupported: [Scope.OPENID], + subjectTypesSupported: [SubjectType.PAIRWISE], + idTokenSigningAlgValuesSupported: [SigningAlgo.ES256], + requestObjectSigningAlgValuesSupported: [SigningAlgo.ES256], + passBy: PassBy.VALUE, + vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.ES256] }, jwt_vp: { alg: [SigningAlgo.ES256] } }, +} + +export function getSupportedDidMethods(agentContext: AgentContext) { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const supportedDidMethods: Set = new Set() + + for (const resolver of didsApi.config.resolvers) { + resolver.supportedMethods.forEach((method) => supportedDidMethods.add(method)) + } + + return Array.from(supportedDidMethods) +} + /** * SIOPv2 Authorization Request with a single v1 presentation definition */ @@ -44,21 +102,243 @@ function isVerifiedAuthorizationRequestWithPresentationDefinition( ) } +// TODO: duplicate +/** + * Returns the JWA Signature Algorithms that are supported by the wallet. + * + * This is an approximation based on the supported key types of the wallet. + * This is not 100% correct as a supporting a key type does not mean you support + * all the algorithms for that key type. However, this needs refactoring of the wallet + * that is planned for the 0.5.0 release. + */ +function getSupportedJwaSignatureAlgorithms(agentContext: AgentContext): JwaSignatureAlgorithm[] { + const supportedKeyTypes = agentContext.wallet.supportedKeyTypes + + // Extract the supported JWS algs based on the key types the wallet support. + const supportedJwaSignatureAlgorithms = supportedKeyTypes + // Map the supported key types to the supported JWK class + .map(getJwkClassFromKeyType) + // Filter out the undefined values + .filter((jwkClass): jwkClass is Exclude => jwkClass !== undefined) + // Extract the supported JWA signature algorithms from the JWK class + .flatMap((jwkClass) => jwkClass.supportedSignatureAlgorithms) + + return supportedJwaSignatureAlgorithms +} + +export function getResolver(agentContext: AgentContext) { + return { + resolve: async (didUrl: string) => { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const result = await didsApi.resolve(didUrl) + + return { + ...result, + didDocument: result.didDocument?.toJSON() as DIDDocument, + } + }, + } +} + @injectable() export class OpenId4VpHolderService { - public constructor(private presentationExchangeService: PresentationExchangeService) {} + private logger: Logger + private jwsService: JwsService + public constructor( + @inject(InjectionSymbols.Logger) logger: Logger, + jwsService: JwsService, + private presentationExchangeService: PresentationExchangeService + ) { + this.jwsService = jwsService + this.logger = logger + } - private getOp(agentContext: AgentContext) { - const supportedDidMethods = this.getSupportedDidMethods(agentContext) + public async getRelyingParty( + agentContext: AgentContext, + options: { + verificationMethod: VerificationMethod | ((clientMetadata: ClientMetadataOpts) => VerificationMethod) + clientMetadata?: ClientMetadataOpts & { authorization_endpoint?: string } + issuer?: string + redirect_url: string + } + ) { + const { verificationMethod: _verificationMethod, issuer, redirect_url } = options + + const supportedDidMethods = getSupportedDidMethods(agentContext) + + // authorization_endpoint + // TODO: + const isVpRequest = false + + let clientMetadata: ClientMetadataOpts & { authorization_endpoint?: string } + if (options.clientMetadata) { + // use the provided client metadata + clientMetadata = options.clientMetadata + } else if (issuer) { + // Use OpenId Discovery to get the client metadata + let reference_uri = issuer + if (!issuer.endsWith('/.well-known/openid-configuration')) { + reference_uri = issuer + '/.well-known/openid-configuration' + } + clientMetadata = { reference_uri, passBy: PassBy.REFERENCE } + } else if (isVpRequest) { + // if neither clientMetadata nor issuer is provided, use a static config + clientMetadata = staticOpOpenIdConfig + } else { + // if neither clientMetadata nor issuer is provided, use a static config + clientMetadata = staticOpSiopConfig + } + + let verificationMethod: VerificationMethod + if (typeof _verificationMethod === 'function') { + verificationMethod = _verificationMethod(clientMetadata) + } else { + verificationMethod = _verificationMethod + } + + const { signature, did, kid, alg } = await this.getSuppliedSignatureFromVerificationMethod( + agentContext, + verificationMethod + ) + + // Check if the OpenId Provider (Holder) can validate the request signature provided by the Relying Party (Verifier) + const requestObjectSigningAlgValuesSupported = clientMetadata.requestObjectSigningAlgValuesSupported + if (requestObjectSigningAlgValuesSupported && !requestObjectSigningAlgValuesSupported.includes(alg)) { + throw new AriesFrameworkError( + [ + `Cannot sign authorization request with '${alg}' that isn't supported by the OpenId Provider.`, + `Supported algorithms are ${requestObjectSigningAlgValuesSupported}`, + ].join('\n') + ) + } + + // Check if the Relying Party (Verifier) can validate the IdToken provided by the OpenId Provider (Holder) + const idTokenSigningAlgValuesSupported = clientMetadata.idTokenSigningAlgValuesSupported + const rpSupportedSignatureAlgorithms = getSupportedJwaSignatureAlgorithms(agentContext) as unknown as SigningAlgo[] + + if (idTokenSigningAlgValuesSupported) { + const possibleIdTokenSigningAlgValues = Array.isArray(idTokenSigningAlgValuesSupported) + ? idTokenSigningAlgValuesSupported.filter((value) => rpSupportedSignatureAlgorithms.includes(value)) + : [idTokenSigningAlgValuesSupported].filter((value) => rpSupportedSignatureAlgorithms.includes(value)) + + if (!possibleIdTokenSigningAlgValues) { + throw new AriesFrameworkError( + [ + `The OpenId Provider supports no signature algorithms that are supported by the Relying Party.`, + `Relying Party supported algorithms are ${rpSupportedSignatureAlgorithms}.`, + `OpenId Provider supported algorithms are ${idTokenSigningAlgValuesSupported}.`, + ].join('\n') + ) + } + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const presentationVerificationCallback: PresentationVerificationCallback = async (x) => { + // TODO: + this.logger.info('verifying presentation') + return Promise.resolve({ + verified: true, + }) + } + + const builder = RP.builder() + // .withClientId('client-id') // The client-id is set to the relying party did + .withRedirectUri(redirect_url) + .withRequestByValue() + .withPresentationVerification(presentationVerificationCallback) + .withIssuer(ResponseIss.SELF_ISSUED_V2) // TODO: this must be set to the issuer with dynamic disc // REQUIRED. URL using the https scheme with no query or fragment component that the Self-Issued OP asserts as its Issuer Identifier. MUST be identical to the iss Claim value in ID Tokens issued from this Self-Issued OP. + .withSuppliedSignature(signature, did, kid, alg) + .withRevocationVerification(RevocationVerification.NEVER) + .withSupportedVersions(SupportedVersion.SIOPv2_D11) + .withClientMetadata(clientMetadata) + .withCustomResolver(getResolver(agentContext)) + .withResponseMode(ResponseMode.POST) + .withResponseType(isVpRequest ? ResponseType.VP_TOKEN : ResponseType.ID_TOKEN) + // .withCheckLinkedDomain + // .withEventEmitter + // .withPresentationDefinition + // .withPresentationVerification + // .withSessionManager + + if (clientMetadata.authorizationEndpoint) { + builder.withAuthorizationEndpoint(clientMetadata.authorizationEndpoint) + } + + for (const supportedDidMethod of supportedDidMethods) { + builder.addDidMethod(supportedDidMethod) + } + + return builder.build() + } + + private async getSuppliedSignatureFromVerificationMethod( + agentContext: AgentContext, + verificationMethod: VerificationMethod + ) { + // get the key from the verification method and use the first supported signature algorithm + const key = getKeyFromVerificationMethod(verificationMethod) + const alg = getJwkClassFromKeyType(key.keyType)?.supportedSignatureAlgorithms[0] + if (!alg) throw new AriesFrameworkError(`No supported signature algorithms for key type: ${key.keyType}`) + + const suppliedSignature = { + signature: async (data: string | Uint8Array) => { + if (typeof data === 'string') { + const signedData = await agentContext.wallet.sign({ + data: typeof data === 'string' ? TypedArrayEncoder.fromString(data) : data, + key, + }) + + const signature = TypedArrayEncoder.toBase64URL(signedData) + + return signature + } + throw new AriesFrameworkError('TODO: this should not hjappen') + }, + // FIXME: cast + alg: alg as unknown as SigningAlgo, + did: verificationMethod.controller, + kid: verificationMethod.id, + } + + return suppliedSignature + } + + private async getOpenIdProvider( + agentContext: AgentContext, + options: { + verificationMethod?: VerificationMethod | (() => VerificationMethod) + } + ) { + const { verificationMethod: _verificationMethod } = options const builder = OP.builder() + .withExpiresIn(6000) + // TODO: + .withIssuer(ResponseIss.SELF_ISSUED_V2) .withResponseMode(ResponseMode.POST) - .withSupportedVersions([SupportedVersion.SIOPv2_ID1]) - .withExpiresIn(300) + .withSupportedVersions([SupportedVersion.SIOPv2_D11]) .withCheckLinkedDomain(CheckLinkedDomain.NEVER) - .withCustomResolver(this.getResolver(agentContext)) + .withCustomResolver(getResolver(agentContext)) + + let verificationMethod: VerificationMethod + + if (_verificationMethod) { + if (typeof _verificationMethod === 'function') { + verificationMethod = _verificationMethod() + } else { + verificationMethod = _verificationMethod + } + + const { signature, did, kid, alg } = await this.getSuppliedSignatureFromVerificationMethod( + agentContext, + verificationMethod + ) + + builder.withSuppliedSignature(signature, did, kid, alg) + } // Add did methods + const supportedDidMethods = getSupportedDidMethods(agentContext) for (const supportedDidMethod of supportedDidMethods) { builder.addDidMethod(supportedDidMethod) } @@ -68,18 +348,68 @@ export class OpenId4VpHolderService { return op } + public async resolveAuthorizationRequest( + agentContext: AgentContext, + requestJwtOrUri: string, + options?: { + correlationId: string + } + ) { + const { correlationId } = options ?? {} + const op = await this.getOpenIdProvider(agentContext, {}) + + // parsing happens automatically in verifyAuthorizationRequest + const verifiedRequest = await op.verifyAuthorizationRequest(requestJwtOrUri, { + // correlationId, + //verification: { + //mode: VerificationMode.EXTERNAL, + //resolveOpts: { resolver: getResolver(agentContext), noUniversalResolverFallback: false }, + //}, + }) + + this.logger.debug(`verified SIOP Authorization Request for issuer '${verifiedRequest.issuer}'`) + this.logger.debug(`requestJwtOrUri '${requestJwtOrUri}'`) + + const presentationDefs = verifiedRequest.presentationDefinitions + if (presentationDefs !== undefined && presentationDefs.length > 0) { + throw new AriesFrameworkError('Not supported yet') + } + + return verifiedRequest + } + + public async acceptRequest( + agentContext: AgentContext, + verifiedReq: VerifiedAuthorizationRequest, + verificationMethod: VerificationMethod + ) { + const op = await this.getOpenIdProvider(agentContext, { verificationMethod }) + + const suppliedSignature = await this.getSuppliedSignatureFromVerificationMethod(agentContext, verificationMethod) + + // TODO: presentations + const authRespWithJWT = await op.createAuthorizationResponse(verifiedReq, { + signature: suppliedSignature, + // https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-aud-of-a-request-object + audience: 'https://acme.com/hello', + }) + return authRespWithJWT + //const response = await op.submitAuthorizationResponse(authRespWithJWT) + //return response + } + public async selectCredentialForProofRequest( agentContext: AgentContext, options: { authorizationRequest: string | URI } ) { - const op = this.getOp(agentContext) + const op = await this.getOpenIdProvider(agentContext, {}) const verification = { mode: VerificationMode.EXTERNAL, resolveOpts: { - resolver: this.getResolver(agentContext), + resolver: getResolver(agentContext), noUniversalResolverFallback: true, }, } satisfies Verification @@ -118,7 +448,7 @@ export class OpenId4VpHolderService { submissionEntryIndexes: number[] } ) { - const op = this.getOp(agentContext) + const op = await this.getOpenIdProvider(agentContext, {}) const credentialsForInputDescriptor: CredentialsForInputDescriptor = {} @@ -137,44 +467,27 @@ export class OpenId4VpHolderService { } }) - const vps = await this.presentationExchangeService.createPresentation(agentContext, { - credentialsForInputDescriptor, - presentationDefinition: options.verifiedAuthorizationRequest.presentationDefinitions[0].definition, - includePresentationSubmissionInVp: false, - // TODO: are there other properties we need to include? - nonce: await options.verifiedAuthorizationRequest.authorizationRequest.getMergedProperty('nonce'), - }) + const { verifiablePresentations, presentationSubmission } = + await this.presentationExchangeService.createPresentation(agentContext, { + credentialsForInputDescriptor, + presentationDefinition: options.verifiedAuthorizationRequest.presentationDefinitions[0].definition, + includePresentationSubmissionInVp: false, + // TODO: are there other properties we need to include? + nonce: await options.verifiedAuthorizationRequest.authorizationRequest.getMergedProperty('nonce'), + }) const verificationMethod = await this.getVerificationMethodFromVerifiablePresentation( agentContext, - vps.verifiablePresentations[0] as W3cVerifiablePresentation + verifiablePresentations[0] as W3cVerifiablePresentation ) - const key = getKeyFromVerificationMethod(verificationMethod) - const alg = getJwkClassFromKeyType(key.keyType)?.supportedSignatureAlgorithms[0] - if (!alg) { - throw new AriesFrameworkError(`No supported algs for key type: ${key.keyType}`) - } const response = await op.createAuthorizationResponse(options.verifiedAuthorizationRequest, { issuer: verificationMethod.controller, presentationExchange: { - verifiablePresentations: vps.verifiablePresentations.map((vp) => vp.encoded as W3CVerifiablePresentation), - presentationSubmission: vps.presentationSubmission, - }, - signature: { - signature: async (data) => { - const signature = await agentContext.wallet.sign({ - data: typeof data === 'string' ? TypedArrayEncoder.fromString(data) : Buffer.from(data), - key, - }) - - return TypedArrayEncoder.toBase64URL(signature) - }, - // FIXME: cast - alg: alg as unknown as SigningAlgo, - did: verificationMethod.controller, - kid: verificationMethod.id, + verifiablePresentations: verifiablePresentations.map((vp) => vp.encoded as W3CVerifiablePresentation), + presentationSubmission, }, + signature: await this.getSuppliedSignatureFromVerificationMethod(agentContext, verificationMethod), }) const responseToResponse = await op.submitAuthorizationResponse(response) @@ -184,31 +497,6 @@ export class OpenId4VpHolderService { } } - private getSupportedDidMethods(agentContext: AgentContext) { - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - const supportedDidMethods: string[] = [] - - for (const resolver of didsApi.config.resolvers) { - supportedDidMethods.push(...resolver.supportedMethods) - } - - return supportedDidMethods - } - - private getResolver(agentContext: AgentContext) { - return { - resolve: async (didUrl: string) => { - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - const result = await didsApi.resolve(didUrl) - - return { - ...result, - didDocument: result.didDocument?.toJSON() as DIDDocument, - } - }, - } - } - // TODO: we can do this in a simpler way, as we're now resolving it multiple times private async getVerificationMethodFromVerifiablePresentation( agentContext: AgentContext, @@ -216,21 +504,21 @@ export class OpenId4VpHolderService { ) { const didsApi = agentContext.dependencyManager.resolve(DidsApi) - let verificationMethod: string + let verificationMethodId: string if (verifiablePresentation instanceof W3cJsonLdVerifiablePresentation) { const [firstProof] = asArray(verifiablePresentation.proof) if (!firstProof) { throw new AriesFrameworkError('Verifiable presentation does not contain a proof') } - verificationMethod = firstProof.verificationMethod + verificationMethodId = firstProof.verificationMethod } else { // FIXME: cast - verificationMethod = verifiablePresentation.jwt.header.kid as string + verificationMethodId = verifiablePresentation.jwt.header.kid as string } - const didDocument = await didsApi.resolveDidDocument(verificationMethod) + const didDocument = await didsApi.resolveDidDocument(verificationMethodId) - return didDocument.dereferenceKey(verificationMethod, ['authentication']) + return didDocument.dereferenceKey(verificationMethodId, ['authentication']) } } diff --git a/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts b/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts index f3060b6515..f5ec68323e 100644 --- a/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts +++ b/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts @@ -23,7 +23,6 @@ import { getKeyFromVerificationMethod, injectable, JsonTransformer, - utils, W3cCredentialService, W3cPresentation, W3cCredentialRepository, @@ -97,10 +96,6 @@ export class PresentationExchangeService { includePresentationSubmissionInVp?: boolean } ) { - // if (selectedCredentials.length === 0) { - // throw new AriesFrameworkError('No credentials selected for creating presentation.') - // } - const vps: { [subjectId: string]: { [inputDescriptorId: string]: W3cVerifiableCredential[] @@ -196,6 +191,7 @@ export class PresentationExchangeService { ...vp.presentationSubmission.descriptor_map.map((descriptor): Descriptor => { const index = verifiablePresentationResults.indexOf(vp) const prefix = verifiablePresentationResults.length > 1 ? `$[${index}]` : '$' + // TODO: use enum instead opf jwt_vp | jwt_vc_json return { format: 'jwt_vp', path: prefix, @@ -269,7 +265,7 @@ export class PresentationExchangeService { verificationMethod: verificationMethod.id, presentation: w3cPresentation, alg, - challenge: challenge ?? nonce ?? utils.uuid(), + challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), domain, }) diff --git a/packages/openid4vc-holder/src/presentations/selection/PexCredentialSelection.ts b/packages/openid4vc-holder/src/presentations/selection/PexCredentialSelection.ts index 702a4411c8..b538b33045 100644 --- a/packages/openid4vc-holder/src/presentations/selection/PexCredentialSelection.ts +++ b/packages/openid4vc-holder/src/presentations/selection/PexCredentialSelection.ts @@ -280,6 +280,7 @@ function getSubmissionForInputDescriptor( ) let name = inputDescriptor.name + //TODO: I think this is something that should be done by the user and not the framework. Might miss some details as to why we do this though. // If there's no name on the input descriptor, but the id does not contain // any special characters or numbers (so only letters and spaces), // we will use a sanitized version of the id as the name diff --git a/packages/openid4vc-holder/src/presentations/transform.ts b/packages/openid4vc-holder/src/presentations/transform.ts index 1cba1b91cc..edb837d66c 100644 --- a/packages/openid4vc-holder/src/presentations/transform.ts +++ b/packages/openid4vc-holder/src/presentations/transform.ts @@ -14,8 +14,6 @@ import { W3cJsonLdVerifiableCredential, } from '@aries-framework/core' -export type { SphereonW3cVerifiableCredential, SphereonW3cVerifiablePresentation } - export function getSphereonW3cVerifiableCredential( w3cVerifiableCredential: W3cVerifiableCredential ): SphereonW3cVerifiableCredential { diff --git a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts index c8ddb91b77..5288b4e278 100644 --- a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts @@ -656,4 +656,42 @@ describe('OpenId4VcHolder', () => { expect(w3cCredentialRecords).toHaveLength(1) }) }) + + //it('authorization code flow https://portal.walt.id/', async () => { + // const did = await agent.dids.create({ + // method: 'key', + // options: { keyType: KeyType.Ed25519 }, + // secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, + // }) + + // const credentialOffer = `` + + // const didKey = DidKey.fromDid(did.didState.did as string) + // const kid = `${didKey.did}#${didKey.key.fingerprint}` + // const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) + // if (!verificationMethod) throw new Error('No verification method found') + + // const resolved = await agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) + + // const resolvedAuthorizationRequest = await agent.modules.openId4VcHolder.resolveAuthorizationRequest(resolved, { + // clientId: 'test-client', + // redirectUri: 'http://blank', + // }) + + // const code = + // 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJiMGUxNjc4NS1kNzIyLTQyYTUtYTA0Zi00YmVhYjI4ZTAzZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.ibEpHFaHFBLWyhEf4SotDQZBeh_FMrfncWapNox1Iv1kdQWQ2cLQeS1VrCyVmPsbx0tN2MAyDFG7DnAaq8MiAA' + + // const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( + // resolved, + // resolvedAuthorizationRequest, + // code, + // { + // allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + // proofOfPossessionVerificationMethodResolver: () => verificationMethod, + // verifyCredentialStatus: false, + // } + // ) + + // expect(w3cCredentialRecords).toHaveLength(1) + //}) }) diff --git a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts index 07a3bcbb45..4413bdd339 100644 --- a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts @@ -1,83 +1,200 @@ -import type { W3cCredentialRecord } from '@aries-framework/core' +import type { KeyDidCreateOptions, VerificationMethod } from '@aries-framework/core' import { AskarModule } from '@aries-framework/askar' -import { KeyType, W3cJwtVerifiableCredential, Agent, Buffer } from '@aries-framework/core' +import { KeyType, Agent, TypedArrayEncoder, DidKey } from '@aries-framework/core' import { agentDependencies } from '@aries-framework/node' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' +import { PassBy, ResponseType, Scope, SigningAlgo, SubjectType } from '@sphereon/did-auth-siop' +import nock from 'nock' -import { OpenId4VcHolderModule, OpenId4VpHolderService } from '../src' +import { OpenId4VcHolderModule } from '../src' +import { getSupportedDidMethods, staticOpSiopConfig } from '../src/presentations/OpenId4VpHolderService' const modules = { - openId4VcClient: new OpenId4VcHolderModule(), - askar: new AskarModule({ - ariesAskar, - }), + openId4VcHolder: new OpenId4VcHolderModule(), + askar: new AskarModule({ ariesAskar }), } -describe('OpenId4VcClient | OpenID4VP', () => { - let agent: Agent +describe('OpenId4VcHolder | OpenID4VP', () => { + let rp: Agent + let rpVerificationMethod: VerificationMethod + + let op: Agent + let opVerificationMethod: VerificationMethod beforeEach(async () => { - agent = new Agent({ + rp = new Agent({ config: { - label: 'OpenId4VcClient OpenID4VP Test', + label: 'OpenId4VcRp OpenID4VP Test', walletConfig: { - id: 'openid4vc-client-openid4vp-test', - key: 'openid4vc-client-openid4vp-test', + id: 'openid4vc-rp-openid4vp-test', + key: 'openid4vc-rp-openid4vp-test', }, }, dependencies: agentDependencies, modules, }) - await agent.initialize() + op = new Agent({ + config: { + label: 'OpenId4VcOp OpenID4VP Test', + walletConfig: { + id: 'openid4vc-op-openid4vp-test', + key: 'openid4vc-op-openid4vp-test', + }, + }, + dependencies: agentDependencies, + modules, + }) + + await rp.initialize() + await op.initialize() + + const rpDid = await rp.dids.create({ + method: 'key', + options: { keyType: KeyType.P256 }, + secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, + }) + + const rpDidKey = DidKey.fromDid(rpDid.didState.did as string) + const rpKid = `${rpDid.didState.did as string}#${rpDidKey.key.fingerprint}` + const _rpVerificationMethod = rpDid.didState.didDocument?.dereferenceKey(rpKid, ['authentication']) + if (!_rpVerificationMethod) throw new Error('No verification method found') + rpVerificationMethod = _rpVerificationMethod + + const opDid = await op.dids.create({ + method: 'key', + options: { keyType: KeyType.P256 }, + secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598f') }, + }) + + const opDidKey = DidKey.fromDid(opDid.didState.did as string) + const opKid = `${opDid.didState.did as string}#${opDidKey.key.fingerprint}` + const _opVerificationMethod = opDid.didState.didDocument?.dereferenceKey(opKid, ['authentication']) + if (!_opVerificationMethod) throw new Error('No verification method found') + opVerificationMethod = _opVerificationMethod }) afterEach(async () => { - await agent.shutdown() - await agent.wallet.delete() + await op.shutdown() + await op.wallet.delete() + await rp.shutdown() + await rp.wallet.delete() }) describe('Mattr interop', () => { // Not working yet. Once it works, we can mock the requests/responses - xit('Should succesfuly share a proof with MATTR launchpad', async () => { - // Store needed credential / did / key - await agent.w3cCredentials.storeCredential({ - credential: W3cJwtVerifiableCredential.fromSerializedJwt( - 'eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bGF1bmNocGFkLnZpaS5lbGVjdHJvbi5tYXR0cmxhYnMuaW8jNkJoRk1DR1RKZyJ9.eyJpc3MiOiJkaWQ6d2ViOmxhdW5jaHBhZC52aWkuZWxlY3Ryb24ubWF0dHJsYWJzLmlvIiwic3ViIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJuYmYiOjE2OTYwMjI5NDksImV4cCI6MTcyNzY0NTM0OSwidmMiOnsibmFtZSI6IkV4YW1wbGUgVW5pdmVyc2l0eSBEZWdyZWUiLCJkZXNjcmlwdGlvbiI6IkpGRiBQbHVnZmVzdCAzIE9wZW5CYWRnZSBDcmVkZW50aWFsIiwiY3JlZGVudGlhbEJyYW5kaW5nIjp7ImJhY2tncm91bmRDb2xvciI6IiM0NjRjNDkifSwiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL21hdHRyLmdsb2JhbC9jb250ZXh0cy92Yy1leHRlbnNpb25zL3YyIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjIuanNvbiIsImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9leHRlbnNpb25zLmpzb24iLCJodHRwczovL3czaWQub3JnL3ZjLXJldm9jYXRpb24tbGlzdC0yMDIwL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJPcGVuQmFkZ2VDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJ0eXBlIjpbIkFjaGlldmVtZW50U3ViamVjdCJdLCJhY2hpZXZlbWVudCI6eyJpZCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vYWNoaWV2ZW1lbnRzLzIxc3QtY2VudHVyeS1za2lsbHMvdGVhbXdvcmsiLCJuYW1lIjoiVGVhbXdvcmsiLCJ0eXBlIjpbIkFjaGlldmVtZW50Il0sImltYWdlIjp7ImlkIjoiaHR0cHM6Ly93M2MtY2NnLmdpdGh1Yi5pby92Yy1lZC9wbHVnZmVzdC0zLTIwMjMvaW1hZ2VzL0pGRi1WQy1FRFUtUExVR0ZFU1QzLWJhZGdlLWltYWdlLnBuZyIsInR5cGUiOiJJbWFnZSJ9LCJjcml0ZXJpYSI6eyJuYXJyYXRpdmUiOiJUZWFtIG1lbWJlcnMgYXJlIG5vbWluYXRlZCBmb3IgdGhpcyBiYWRnZSBieSB0aGVpciBwZWVycyBhbmQgcmVjb2duaXplZCB1cG9uIHJldmlldyBieSBFeGFtcGxlIENvcnAgbWFuYWdlbWVudC4ifSwiZGVzY3JpcHRpb24iOiJUaGlzIGJhZGdlIHJlY29nbml6ZXMgdGhlIGRldmVsb3BtZW50IG9mIHRoZSBjYXBhY2l0eSB0byBjb2xsYWJvcmF0ZSB3aXRoaW4gYSBncm91cCBlbnZpcm9ubWVudC4ifX0sImlzc3VlciI6eyJpZCI6ImRpZDp3ZWI6bGF1bmNocGFkLnZpaS5lbGVjdHJvbi5tYXR0cmxhYnMuaW8iLCJuYW1lIjoiRXhhbXBsZSBVbml2ZXJzaXR5IiwiaWNvblVybCI6Imh0dHBzOi8vdzNjLWNjZy5naXRodWIuaW8vdmMtZWQvcGx1Z2Zlc3QtMS0yMDIyL2ltYWdlcy9KRkZfTG9nb0xvY2t1cC5wbmciLCJpbWFnZSI6Imh0dHBzOi8vdzNjLWNjZy5naXRodWIuaW8vdmMtZWQvcGx1Z2Zlc3QtMS0yMDIyL2ltYWdlcy9KRkZfTG9nb0xvY2t1cC5wbmcifX19.HUYvivfEH2-yBXUq6t5gEZu1NY7_6tjsWojQvYbpRL_md5TyAmwn-LyfcPLyrQpgJcu08XjFp8smXFMfYJEqCQ' - ), - }) + // xit('Should succesfuly share a proof with MATTR launchpad', async () => { + // // Store needed credential / did / key + // await agent.w3cCredentials.storeCredential({ + // credential: W3cJwtVerifiableCredential.fromSerializedJwt( + // 'eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bGF1bmNocGFkLnZpaS5lbGVjdHJvbi5tYXR0cmxhYnMuaW8jNkJoRk1DR1RKZyJ9.eyJpc3MiOiJkaWQ6d2ViOmxhdW5jaHBhZC52aWkuZWxlY3Ryb24ubWF0dHJsYWJzLmlvIiwic3ViIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJuYmYiOjE2OTYwMjI5NDksImV4cCI6MTcyNzY0NTM0OSwidmMiOnsibmFtZSI6IkV4YW1wbGUgVW5pdmVyc2l0eSBEZWdyZWUiLCJkZXNjcmlwdGlvbiI6IkpGRiBQbHVnZmVzdCAzIE9wZW5CYWRnZSBDcmVkZW50aWFsIiwiY3JlZGVudGlhbEJyYW5kaW5nIjp7ImJhY2tncm91bmRDb2xvciI6IiM0NjRjNDkifSwiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL21hdHRyLmdsb2JhbC9jb250ZXh0cy92Yy1leHRlbnNpb25zL3YyIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjIuanNvbiIsImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9leHRlbnNpb25zLmpzb24iLCJodHRwczovL3czaWQub3JnL3ZjLXJldm9jYXRpb24tbGlzdC0yMDIwL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJPcGVuQmFkZ2VDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJ0eXBlIjpbIkFjaGlldmVtZW50U3ViamVjdCJdLCJhY2hpZXZlbWVudCI6eyJpZCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vYWNoaWV2ZW1lbnRzLzIxc3QtY2VudHVyeS1za2lsbHMvdGVhbXdvcmsiLCJuYW1lIjoiVGVhbXdvcmsiLCJ0eXBlIjpbIkFjaGlldmVtZW50Il0sImltYWdlIjp7ImlkIjoiaHR0cHM6Ly93M2MtY2NnLmdpdGh1Yi5pby92Yy1lZC9wbHVnZmVzdC0zLTIwMjMvaW1hZ2VzL0pGRi1WQy1FRFUtUExVR0ZFU1QzLWJhZGdlLWltYWdlLnBuZyIsInR5cGUiOiJJbWFnZSJ9LCJjcml0ZXJpYSI6eyJuYXJyYXRpdmUiOiJUZWFtIG1lbWJlcnMgYXJlIG5vbWluYXRlZCBmb3IgdGhpcyBiYWRnZSBieSB0aGVpciBwZWVycyBhbmQgcmVjb2duaXplZCB1cG9uIHJldmlldyBieSBFeGFtcGxlIENvcnAgbWFuYWdlbWVudC4ifSwiZGVzY3JpcHRpb24iOiJUaGlzIGJhZGdlIHJlY29nbml6ZXMgdGhlIGRldmVsb3BtZW50IG9mIHRoZSBjYXBhY2l0eSB0byBjb2xsYWJvcmF0ZSB3aXRoaW4gYSBncm91cCBlbnZpcm9ubWVudC4ifX0sImlzc3VlciI6eyJpZCI6ImRpZDp3ZWI6bGF1bmNocGFkLnZpaS5lbGVjdHJvbi5tYXR0cmxhYnMuaW8iLCJuYW1lIjoiRXhhbXBsZSBVbml2ZXJzaXR5IiwiaWNvblVybCI6Imh0dHBzOi8vdzNjLWNjZy5naXRodWIuaW8vdmMtZWQvcGx1Z2Zlc3QtMS0yMDIyL2ltYWdlcy9KRkZfTG9nb0xvY2t1cC5wbmciLCJpbWFnZSI6Imh0dHBzOi8vdzNjLWNjZy5naXRodWIuaW8vdmMtZWQvcGx1Z2Zlc3QtMS0yMDIyL2ltYWdlcy9KRkZfTG9nb0xvY2t1cC5wbmcifX19.HUYvivfEH2-yBXUq6t5gEZu1NY7_6tjsWojQvYbpRL_md5TyAmwn-LyfcPLyrQpgJcu08XjFp8smXFMfYJEqCQ' + // ), + // }) + // see https://github.com/hyperledger/aries-framework-javascript/pull/1604#discussion_r1376347318 + // const key = await op.wallet.createKey({ + // keyType: KeyType.Ed25519, + // privateKey: TypedArrayEncoder.fromString('00000000000000000000000000000000'), + // }) + // const did = new DidKey(key) + // await agent.dids.import({ + // did: 'did:key:z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + // }) + // const openId4VpHolderService = agent.dependencyManager.resolve(OpenId4VpHolderService) + // const { selectResults, verifiedAuthorizationRequest } = + // await openId4VpHolderService.selectCredentialForProofRequest(agent.context, { + // authorizationRequest: + // 'openid4vp://authorize?client_id=https%3A%2F%2Flaunchpad.mattrlabs.com%2Fapi%2Fvp%2Fcallback&client_id_scheme=redirect_uri&response_uri=https%3A%2F%2Flaunchpad.mattrlabs.com%2Fapi%2Fvp%2Fcallback&response_type=vp_token&response_mode=direct_post&presentation_definition_uri=https%3A%2F%2Flaunchpad.mattrlabs.com%2Fapi%2Fvp%2Frequest%3Fstate%3D9b2nQuoLQkW0bX_vk24qjg&nonce=u-Wg1dR5wo5IqIr8ilshMQ&state=9b2nQuoLQkW0bX_vk24qjg', + // }) + // if (!selectResults.areRequirementsSatisfied) { + // throw new Error('Requirements are not satisfied.') + // } + // const credentialRecords = selectResults.requirements + // .flatMap((requirement) => requirement.submission.flatMap((submission) => submission.verifiableCredentials)) + // .filter((credentialRecord): credentialRecord is W3cCredentialRecord => credentialRecord !== undefined) + // const credentials = credentialRecords.map((credentialRecord) => credentialRecord.credential) + // //await openId4VpHolderService.shareProof(agent.context, { + // // verifiedAuthorizationRequest, + // // selectedCredentials: credentials, + // //}) + // }) - await agent.wallet.createKey({ - keyType: KeyType.Ed25519, - seed: Buffer.from('00000000000000000000000000000000'), - }) + xit('test against sphereon itself', async () => { + const clientMetadata = { + subject_syntax_types_supported: getSupportedDidMethods(rp.context), + responseTypesSupported: [ResponseType.ID_TOKEN], + scopesSupported: [Scope.OPENID], + subjectTypesSupported: [SubjectType.PAIRWISE], + idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256], + requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256], + passBy: PassBy.VALUE, + } - await agent.dids.import({ - did: 'did:key:z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + //////////////////////////// RP (create request) //////////////////////////// + const { authorizationRequestUri, relyingParty } = await rp.modules.openId4VcHolder.createRequest({ + verificationMethod: rpVerificationMethod, + redirect_url: 'https://acme.com/hello', + // TODO: if provided this way client metadata is not resolved vor the verification method + clientMetadata: clientMetadata, }) - const openId4VpHolderService = agent.dependencyManager.resolve(OpenId4VpHolderService) - const { selectResults, verifiedAuthorizationRequest } = - await openId4VpHolderService.selectCredentialForProofRequest(agent.context, { - authorizationRequest: - 'openid4vp://authorize?client_id=https%3A%2F%2Flaunchpad.mattrlabs.com%2Fapi%2Fvp%2Fcallback&client_id_scheme=redirect_uri&response_uri=https%3A%2F%2Flaunchpad.mattrlabs.com%2Fapi%2Fvp%2Fcallback&response_type=vp_token&response_mode=direct_post&presentation_definition_uri=https%3A%2F%2Flaunchpad.mattrlabs.com%2Fapi%2Fvp%2Frequest%3Fstate%3D9b2nQuoLQkW0bX_vk24qjg&nonce=u-Wg1dR5wo5IqIr8ilshMQ&state=9b2nQuoLQkW0bX_vk24qjg', - }) + //////////////////////////// OP (validate and parse the request) //////////////////////////// + const result = await op.modules.openId4VcHolder.resolveRequest(authorizationRequestUri) - if (!selectResults.areRequirementsSatisfied) { - throw new Error('Requirements are not satisfied.') - } + //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// + // TODO: User interaction - const credentialRecords = selectResults.requirements - .flatMap((requirement) => requirement.submission.flatMap((submission) => submission.verifiableCredentials)) - .filter((credentialRecord): credentialRecord is W3cCredentialRecord => credentialRecord !== undefined) + //////////////////////////// OP (accept the verified request) //////////////////////////// + const authRespWithJWT = await op.modules.openId4VcHolder.acceptRequest(result, opVerificationMethod) - const credentials = credentialRecords.map((credentialRecord) => credentialRecord.credential) + //////////////////////////// RP (verify the response) //////////////////////////// + const verifiedAuthResponseWithJWT = await relyingParty.verifyAuthorizationResponse( + authRespWithJWT.response.payload, + { + audience: 'https://acme.com/hello', + } + ) - //await openId4VpHolderService.shareProof(agent.context, { - // verifiedAuthorizationRequest, - // selectedCredentials: credentials, - //}) + expect(verifiedAuthResponseWithJWT.idToken).toBeDefined() + expect(verifiedAuthResponseWithJWT.idToken?.payload.state).toMatch('b32f0087fc9816eb813fd11f') + expect(verifiedAuthResponseWithJWT.idToken?.payload.nonce).toMatch('qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg') }) }) + + it('jajaja', async () => { + nock('https://helloworld').get('/.well-known/openid-configuration').reply(200, staticOpSiopConfig) + + //////////////////////////// RP (create request) //////////////////////////// + const { authorizationRequestUri, relyingParty } = await rp.modules.openId4VcHolder.createRequest({ + verificationMethod: rpVerificationMethod, + redirect_url: 'https://acme.com/hello', + // TODO: if provided this way client metadata is not resolved vor the verification method + // TODO: rename to verifierMetadata? + clientMetadata: { + passBy: PassBy.REFERENCE, + reference_uri: 'https://helloworld/.well-known/openid-configuration', + }, + }) + + //////////////////////////// OP (validate and parse the request) //////////////////////////// + const result = await op.modules.openId4VcHolder.resolveRequest(authorizationRequestUri) + + //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// + // TODO: User interaction + + //////////////////////////// OP (accept the verified request) //////////////////////////// + const authRespWithJWT = await op.modules.openId4VcHolder.acceptRequest(result, opVerificationMethod) + + //////////////////////////// RP (verify the response) //////////////////////////// + const verifiedAuthResponseWithJWT = await relyingParty.verifyAuthorizationResponse( + authRespWithJWT.response.payload, + { + audience: 'https://acme.com/hello', + } + ) + + expect(verifiedAuthResponseWithJWT.idToken).toBeDefined() + expect(verifiedAuthResponseWithJWT.idToken?.payload.state).toMatch('b32f0087fc9816eb813fd11f') + expect(verifiedAuthResponseWithJWT.idToken?.payload.nonce).toMatch('qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg') + }) }) diff --git a/packages/openid4vc-issuer/package.json b/packages/openid4vc-issuer/package.json index 3023b6998b..79714a77c5 100644 --- a/packages/openid4vc-issuer/package.json +++ b/packages/openid4vc-issuer/package.json @@ -27,9 +27,7 @@ "@aries-framework/core": "0.4.2", "@sphereon/oid4vci-issuer": "^0.7.3", "@sphereon/oid4vci-common": "^0.7.3", - "@sphereon/ssi-types": "^0.17.5", - "@stablelib/random": "^1.0.2", - "fast-text-encoding": "^1.0.6" + "@sphereon/ssi-types": "^0.17.5" }, "devDependencies": { "@aries-framework/askar": "0.4.2", diff --git a/packages/openid4vc-issuer/src/index.ts b/packages/openid4vc-issuer/src/index.ts index 3be8888448..735d8d1bbf 100644 --- a/packages/openid4vc-issuer/src/index.ts +++ b/packages/openid4vc-issuer/src/index.ts @@ -1,5 +1,3 @@ -import 'fast-text-encoding' - export * from './OpenId4VcIssuerApi' export * from './OpenId4VcIssuerModule' export * from './OpenId4VcIssuerService' diff --git a/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts b/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts index 2aa4596409..aaee28ea80 100644 --- a/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts +++ b/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts @@ -1,22 +1,13 @@ -import type { KeyDidCreateOptions } from '@aries-framework/core' - import { AskarModule } from '@aries-framework/askar' -import { - JwaSignatureAlgorithm, - Agent, - KeyType, - TypedArrayEncoder, - W3cCredentialRecord, - DidKey, -} from '@aries-framework/core' +import { Agent } from '@aries-framework/core' import { agentDependencies } from '@aries-framework/node' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' -import nock, { cleanAll, enableNetConnect } from 'nock' +import { cleanAll, enableNetConnect } from 'nock' import { OpenId4VcIssuerModule } from '../src' const modules = { - openId4VcHolder: new OpenId4VcIssuerModule(), + openId4VcIssuer: new OpenId4VcIssuerModule(), askar: new AskarModule({ ariesAskar, }), diff --git a/packages/openid4vc-verifier/package.json b/packages/openid4vc-verifier/package.json index aba1ebdfef..e59c2d5b0e 100644 --- a/packages/openid4vc-verifier/package.json +++ b/packages/openid4vc-verifier/package.json @@ -26,9 +26,7 @@ "dependencies": { "@aries-framework/core": "0.4.2", "@sphereon/did-auth-siop": "^0.4.2", - "@sphereon/ssi-types": "^0.17.5", - "@stablelib/random": "^1.0.2", - "fast-text-encoding": "^1.0.6" + "@sphereon/ssi-types": "^0.17.5" }, "devDependencies": { "@aries-framework/askar": "0.4.2", diff --git a/packages/openid4vc-verifier/src/index.ts b/packages/openid4vc-verifier/src/index.ts index e13c769e11..f606b366c0 100644 --- a/packages/openid4vc-verifier/src/index.ts +++ b/packages/openid4vc-verifier/src/index.ts @@ -1,5 +1,3 @@ -import 'fast-text-encoding' - export * from './OpenId4VcVerifierApi' export * from './OpenId4VcVerifierModule' export * from './OpenId4VcVerifierService' diff --git a/packages/openid4vc-verifier/tests/openid4vc-verifier.e2e.test.ts b/packages/openid4vc-verifier/tests/openid4vc-verifier.e2e.test.ts index fbc8310e2f..741d6f031a 100644 --- a/packages/openid4vc-verifier/tests/openid4vc-verifier.e2e.test.ts +++ b/packages/openid4vc-verifier/tests/openid4vc-verifier.e2e.test.ts @@ -1,22 +1,13 @@ -import type { KeyDidCreateOptions } from '@aries-framework/core' - import { AskarModule } from '@aries-framework/askar' -import { - JwaSignatureAlgorithm, - Agent, - KeyType, - TypedArrayEncoder, - W3cCredentialRecord, - DidKey, -} from '@aries-framework/core' +import { Agent } from '@aries-framework/core' import { agentDependencies } from '@aries-framework/node' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' -import nock, { cleanAll, enableNetConnect } from 'nock' +import { cleanAll, enableNetConnect } from 'nock' import { OpenId4VcVerifierModule } from '../src' const modules = { - openId4VcHolder: new OpenId4VcVerifierModule(), + openId4VcVerifier: new OpenId4VcVerifierModule(), askar: new AskarModule({ ariesAskar, }), diff --git a/yarn.lock b/yarn.lock index fd6a486bef..9148667c56 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2430,6 +2430,28 @@ uint8arrays "^3.1.1" uuid "^9.0.0" +"@sphereon/did-auth-siop@^0.5.0-unstable.7": + version "0.5.0-unstable.7" + resolved "https://registry.yarnpkg.com/@sphereon/did-auth-siop/-/did-auth-siop-0.5.0-unstable.7.tgz#3867ffe44f9289ce85f1f41241d98464e0acb4c4" + integrity sha512-Yq/NqrRTin85zkJA1//EzZ8j+hwt1Aw5YK2crN0MRhW6Oo3eoAuhLygCJNvy81hkwgawd8MuP9Bf6rk9vHsFsA== + dependencies: + "@astronautlabs/jsonpath" "^1.1.2" + "@sphereon/did-uni-client" "^0.6.0" + "@sphereon/pex" "2.2.1-unstable.0" + "@sphereon/pex-models" "^2.1.1" + "@sphereon/ssi-types" "^0.17.5" + "@sphereon/wellknown-dids-client" "^0.1.3" + cross-fetch "^3.1.8" + did-jwt "6.11.6" + did-resolver "^4.1.0" + events "^3.3.0" + language-tags "^1.0.8" + multiformats "^11.0.2" + qs "^6.11.2" + sha.js "^2.4.11" + uint8arrays "^3.1.1" + uuid "^9.0.0" + "@sphereon/did-uni-client@^0.6.0": version "0.6.0" resolved "https://registry.yarnpkg.com/@sphereon/did-uni-client/-/did-uni-client-0.6.0.tgz#6592e1fc514f277ddbc531fc5095a834a9813030" @@ -2480,28 +2502,28 @@ resolved "https://registry.yarnpkg.com/@sphereon/pex-models/-/pex-models-2.1.1.tgz#399e529db2a7e3b9abbd7314cdba619ceb6cb758" integrity sha512-0UX/CMwgiJSxzuBn6SLOTSKkm+uPq3dkNjl8w4EtppXp6zBB4lQMd1mJX7OifX5Bp5vPUfoz7bj2B+yyDtbZww== -"@sphereon/pex@^2.1.2": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-2.1.2.tgz#99ecaf9dcf62bdbaf3a24db28abc0e165051a894" - integrity sha512-x2lo4iRWfKj2NQIGVZIMhwYrCllRY7j0U9t3g0pkx3mxSUwXhQwEYAcBU+AlS5rGv1kLUXRhHDGPUwt7Y0kHgw== +"@sphereon/pex@2.2.1-unstable.0", "@sphereon/pex@^2.2.1-unstable.0": + version "2.2.1-unstable.0" + resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-2.2.1-unstable.0.tgz#58c339f578d487db5e7ac54a79871c58bdbe63ff" + integrity sha512-gAO4+pUSdN+kDHaB1D7oCSn1AZcKOeH3IXv7NdPkf1U4J/sJpLEXA46gsG8SBTX4e45CRGwt8c5F7vPmIwX7XQ== dependencies: "@astronautlabs/jsonpath" "^1.1.2" - "@sphereon/pex-models" "^2.0.3" - "@sphereon/ssi-types" "^0.15.1" + "@sphereon/pex-models" "^2.1.1" + "@sphereon/ssi-types" "^0.17.5" ajv "^8.12.0" ajv-formats "^2.1.1" jwt-decode "^3.1.2" nanoid "^3.3.6" string.prototype.matchall "^4.0.8" -"@sphereon/pex@^2.1.3-unstable.6": - version "2.1.3-unstable.6" - resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-2.1.3-unstable.6.tgz#0934c07d615551bef2092673824d60f58530378a" - integrity sha512-8vyYwGGqtxfmlwJuSe2lFSI8sedFV8Y6DR1o+bqMe18cUtgJUZ2XlsDLbq9r0npC5URJQJWOUo5ZUoGWuRJfCg== +"@sphereon/pex@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-2.1.2.tgz#99ecaf9dcf62bdbaf3a24db28abc0e165051a894" + integrity sha512-x2lo4iRWfKj2NQIGVZIMhwYrCllRY7j0U9t3g0pkx3mxSUwXhQwEYAcBU+AlS5rGv1kLUXRhHDGPUwt7Y0kHgw== dependencies: "@astronautlabs/jsonpath" "^1.1.2" - "@sphereon/pex-models" "^2.1.1" - "@sphereon/ssi-types" "^0.17.5" + "@sphereon/pex-models" "^2.0.3" + "@sphereon/ssi-types" "^0.15.1" ajv "^8.12.0" ajv-formats "^2.1.1" jwt-decode "^3.1.2" @@ -5812,7 +5834,7 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -fast-text-encoding@^1.0.3, fast-text-encoding@^1.0.6: +fast-text-encoding@^1.0.3: version "1.0.6" resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" integrity sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w== @@ -10465,6 +10487,13 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" +qs@^6.11.2: + version "6.11.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" + integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== + dependencies: + side-channel "^1.0.4" + query-string@^7.0.1: version "7.1.3" resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.3.tgz#a1cf90e994abb113a325804a972d98276fe02328" From 3bef9c074703fa8063387da27cf8981c71788860 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 6 Nov 2023 14:34:46 +0100 Subject: [PATCH 029/115] chore: incorporate feedback Signed-off-by: Martin Auer --- .../src/OpenId4VcHolderApi.ts | 19 ++++++++++--------- .../src/issuance/OpenId4VciHolderService.ts | 12 +++++++----- packages/openid4vc-issuer/src/index.ts | 3 --- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts index 2a83335c06..853f513727 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts @@ -11,7 +11,7 @@ import type { CredentialOfferPayloadV1_0_11 } from '@sphereon/oid4vci-common' import { injectable, AgentContext } from '@aries-framework/core' import { OpenId4VcHolderService } from './issuance/OpenId4VciHolderService' -import { OpenId4VpHolderService } from './presentations/OpenId4VpHolderService' +import { OpenId4VpHolderService } from './presentations' /** * @public @@ -40,10 +40,11 @@ export class OpenId4VcHolderApi { }) { const relyingParty = await this.openId4VpHolderService.getRelyingParty(this.agentContext, options) - // TODO: generate nonce, state, correlationId - const nonce = 'qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg' - const state = 'b32f0087fc9816eb813fd11f' - const correlationId = '1' + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const randomValues = await Promise.all([...Array(4).keys()].map((_) => this.agentContext.wallet.generateNonce())) + const nonce = randomValues[0] + randomValues[1] + const state = randomValues[2] + const correlationId = randomValues[3] const authorizationRequest = await relyingParty.createAuthorizationRequest({ correlationId, @@ -101,7 +102,7 @@ export class OpenId4VcHolderApi { * This function automatically generates the authorization_details for all offered credentials. * If scopes are provided, the provided scopes are send alongside the authorization_details. * - * @param resolvedCredentialOffer Obtained through @function resolveCredentialOffer + * @param resolvedCredentialOffer Obtained through @see resolveCredentialOffer * @param authCodeFlowOptions * @returns The authorization request URI alongside the code verifier and original @param authCodeFlowOptions */ @@ -118,7 +119,7 @@ export class OpenId4VcHolderApi { /** * Accepts a credential offer using the pre-authorized code flow. - * @param resolvedCredentialOffer Obtained through @function resolveCredentialOffer + * @param resolvedCredentialOffer Obtained through @see resolveCredentialOffer * @param acceptCredentialOfferOptions * @returns W3cCredentialRecord[] */ @@ -134,8 +135,8 @@ export class OpenId4VcHolderApi { /** * Accepts a credential offer using the authorization code flow. - * @param resolvedCredentialOffer Obtained through @function resolveCredentialOffer - * @param resolvedAuthorizationRequest Obtained through @function resolveAuthorizationRequest + * @param resolvedCredentialOffer Obtained through @see resolveCredentialOffer + * @param resolvedAuthorizationRequest Obtained through @see resolveAuthorizationRequest * @param code The authorization code obtained via the authorization request URI * @param acceptCredentialOfferOptions * @returns W3cCredentialRecord[] diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts index 96574abd84..95f3a4855b 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts @@ -257,7 +257,7 @@ export class OpenId4VcHolderService { credentialWithMetadata: OfferedCredentialWithMetadata, authDetailsLocation: string | undefined, version: OpenId4VCIVersion - ): AuthDetails { + ): AuthDetails | undefined { const { format, types } = credentialWithMetadata const type = 'openid_credential' @@ -278,7 +278,9 @@ export class OpenId4VcHolderService { let context: string | undefined = undefined if (credentialWithMetadata.offerType === OfferedCredentialType.InlineCredentialOffer) { - // Inline Credential Offers come with no context + // Inline Credential Offers come with no context so we cannot create the authorization_details + // This type of credentials can only be requested via scopes + return undefined } else { if ('@context' in credentialWithMetadata.credentialSupported) { context = credentialWithMetadata.credentialSupported['@context'] as unknown as string @@ -324,9 +326,9 @@ export class OpenId4VcHolderService { authDetailsLocation = metadata.issuer } - const authDetails = offeredCredentialsWithMetadata.map((credential) => - this.getAuthDetailsFromOfferedCredential(credential, authDetailsLocation, version) - ) + const authDetails = offeredCredentialsWithMetadata + .map((credential) => this.getAuthDetailsFromOfferedCredential(credential, authDetailsLocation, version)) + .filter((authDetail): authDetail is AuthDetails => authDetail !== undefined) this.logger.debug('Converted code_verifier to code_challenge', { codeVerifier: codeVerifier, diff --git a/packages/openid4vc-issuer/src/index.ts b/packages/openid4vc-issuer/src/index.ts index 735d8d1bbf..f74959d07c 100644 --- a/packages/openid4vc-issuer/src/index.ts +++ b/packages/openid4vc-issuer/src/index.ts @@ -1,6 +1,3 @@ export * from './OpenId4VcIssuerApi' export * from './OpenId4VcIssuerModule' export * from './OpenId4VcIssuerService' - -// Contains internal types, so we don't export everything -export {} from './OpenId4VcIssuerServiceOptions' From 1bd0c6574bfd237b45cf9e30a40c6b88b64173cf Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 7 Nov 2023 09:31:23 +0100 Subject: [PATCH 030/115] chore: incorporate feedback Signed-off-by: Martin Auer --- packages/openid4vc-holder/README.md | 127 ++++++++++-------- .../src/OpenId4VcHolderServiceOptions.ts | 14 +- .../src/issuance/OpenId4VciHolderService.ts | 10 +- .../src/issuance/utils/IssuerMetadataUtils.ts | 2 +- 4 files changed, 82 insertions(+), 71 deletions(-) diff --git a/packages/openid4vc-holder/README.md b/packages/openid4vc-holder/README.md index 1cb88b2763..a8b02ef296 100644 --- a/packages/openid4vc-holder/README.md +++ b/packages/openid4vc-holder/README.md @@ -81,7 +81,6 @@ const did = await agent.dids.create({ }) // next we do some assertions and extract the key identifier (kid) - if ( !did.didState.didDocument || !did.didState.didDocument.authentication || @@ -96,72 +95,82 @@ const kid = typeof verificationMethod === 'string' ? verificationMethod : verifi #### Requesting the credential (Pre-Authorized) -Now a credential issuance can be requested as follows. - ```ts -const w3cCredentialRecord = await agent.modules.openId4VcHolder.requestCredentialPreAuthorized({ - issuerUri, - kid, - checkRevocationState: false, +// To request credentials(s), you need a credential offer. +// The credential offer be provided as actual payload, +// the credential offer URL or issuance initiation URL +const credentialOffer = + 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fjff.walt.id%2Fissuer-api%2Fdefault%2Foidc%22%2C%22credentials%22%3A%5B%22VerifiableId%22%2C%20%22VerifiableDiploma%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22ABC%22%7D%7D%7D' + +// The first step is to resolve the credential offer and +// get all metadata required for the issuance of the credentials. +const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) + +// The second (optional) step is to filter out the credentials which you want to request. +const selectedCredentialsForRequest = resolvedCredentialOffer.credentialsToRequest.filter((credential) => { + return credential.format === OpenIdCredentialFormatProfile.JwtVcJson && credential.types.includes('VerifiableId') }) -console.log(w3cCredentialRecord) +// The third step is to accept the credential offer. +// If no credentialsToRequest are specified all offered credentials are requested. +const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + resolvedCredentialOffer, + { + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], + proofOfPossessionVerificationMethodResolver: () => verificationMethod, + verifyCredentialStatus: false, + credentialsToRequest: selectedCredentialsForRequest, + } +) + +console.log(w3cCredentialRecords) ``` -#### Full example +#### Requesting the credential (Authorization Code Flow) +Requesting credentials via the Authorization Code Flow function conceptually similar, +except that there is an intermediary step involved to resolve the authorization request, and then manually get the authorization code. ```ts -import { OpenId4VcHolderModule } from '@aries-framework/openid4vc-holder' -import { agentDependencies } from '@aries-framework/node' // use @aries-framework/react-native for React Native -import { Agent, KeyDidCreateOptions } from '@aries-framework/core' - -const run = async () => { - const issuerUri = '' // The obtained issuer URI - - // Create the Agent - const agent = new Agent({ - config: { - /* config */ - }, - dependencies: agentDependencies, - modules: { - openId4VcHolder: new OpenId4VcHolderModule(), - /* other custom modules */ - }, - }) - - // Initialize the Agent - await agent.initialize() - - // Create a DID - const did = await agent.dids.create({ - method: 'key', - options: { - keyType: KeyType.Ed25519, - }, - }) - - // Assert DIDDocument is valid - if ( - !did.didState.didDocument || - !did.didState.didDocument.authentication || - did.didState.didDocument.authentication.length === 0 - ) { - throw new Error("Error creating did document, or did document has no 'authentication' verificationMethods") - } +// To request credentials(s), you need a credential offer. +// The credential offer be provided as actual payload, +// the credential offer URL or issuance initiation URL +const credentialOffer = `openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.portal.walt.id%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22OpenBadgeCredential%22%5D%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fpurl.imsglobal.org%2Fspec%2Fob%2Fv3p0%2Fcontext.json%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22OpenBadgeCredential%22%5D%7D%7D%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22b0e16785-d722-42a5-a04f-4beab28e03ea%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJiMGUxNjc4NS1kNzIyLTQyYTUtYTA0Zi00YmVhYjI4ZTAzZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.ibEpHFaHFBLWyhEf4SotDQZBeh_FMrfncWapNox1Iv1kdQWQ2cLQeS1VrCyVmPsbx0tN2MAyDFG7DnAaq8MiAA%22%2C%22user_pin_required%22%3Afalse%7D%7D%7D` + +// The first step is to resolve the credential offer and +// get all metadata required for the issuance of the credentials. +const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) + +// The second step is the resolve the authorization request. +const resolvedAuthorizationRequest = await agent.modules.openId4VcHolder.resolveAuthorizationRequest(resolved, { + clientId: 'test-client', + redirectUri: 'http://blank', + scope: ['openid', 'OpenBadgeCredential'], +}) - // Extract key identified (kid) for authentication verification method - const [verificationMethod] = did.didState.didDocument.authentication - const kid = typeof verificationMethod === 'string' ? verificationMethod : verificationMethod.id +// The resolved authorization request contains the authorizationRequestUri, +// which can be used to obtain the actual authorization code. +// Currently, this needs to be done manually +const code = + 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJiMGUxNjc4NS1kNzIyLTQyYTUtYTA0Zi00YmVhYjI4ZTAzZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.ibEpHFaHFBLWyhEf4SotDQZBeh_FMrfncWapNox1Iv1kdQWQ2cLQeS1VrCyVmPsbx0tN2MAyDFG7DnAaq8MiAA' - // Request the credential - const w3cCredentialRecord = await agent.modules.openId4VcHolder.requestCredentialPreAuthorized({ - issuerUri, - kid, - checkRevocationState: false, - }) +// The third (optional) step is to filter out the credentials which you want to request. +const selectedCredentialsForRequest = resolvedCredentialOffer.credentialsToRequest.filter((credential) => { + return credential.format === OpenIdCredentialFormatProfile.JwtVcJson && credential.types.includes('VerifiableId') +}) - // Log the received credential - console.log(w3cCredentialRecord) -} +// The fourth step is to accept the credential offer. +// If no credentialsToRequest are specified all offered credentials are requested. +const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( + resolvedCredentialOffer, + resolvedAuthorizationRequest, + code, + { + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], + proofOfPossessionVerificationMethodResolver: () => verificationMethod, + verifyCredentialStatus: false, + credentialsToRequest: selectedCredentialsForRequest, + } +) + +console.log(w3cCredentialRecords) ``` diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts index e3e561af39..9781659143 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts @@ -1,19 +1,9 @@ import type { OfferedCredentialType } from './issuance/utils/IssuerMetadataUtils' import type { JwaSignatureAlgorithm, KeyType, VerificationMethod } from '@aries-framework/core' import type { CredentialOfferPayloadV1_0_11, EndpointMetadataResult, OpenId4VCIVersion } from '@sphereon/oid4vci-common' -import type { CredentialFormat } from '@sphereon/ssi-types' import { OpenIdCredentialFormatProfile } from './issuance/utils/claimFormatMapping' -// TODO: use simpler object -export interface AuthDetails { - type: 'openid_credential' | string - locations?: string | string[] - format: CredentialFormat | CredentialFormat[] - - [s: string]: unknown -} - /** * The credential formats that are supported by the openid4vc holder */ @@ -56,6 +46,10 @@ export interface AcceptCredentialOfferOptions { */ userPin?: string + /** + * This is the list of credentials that will be requested from the issuer. + * If not provided all offered credentials will be requested. + */ credentialsToRequest?: CredentialToRequest[] verifyCredentialStatus: boolean diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts index 95f3a4855b..f890d5b8d7 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts @@ -1,7 +1,6 @@ import type { OfferedCredentialWithMetadata } from './utils/IssuerMetadataUtils' import type { AuthCodeFlowOptions, - AuthDetails, CredentialToRequest, AcceptCredentialOfferOptions, ProofOfPossessionRequirements, @@ -12,6 +11,7 @@ import type { } from '../OpenId4VcHolderServiceOptions' import type { AgentContext, + CredentialFormat, JwaSignatureAlgorithm, VerificationMethod, W3cVerifiableCredential, @@ -81,6 +81,14 @@ import { OfferedCredentialType, } from './utils/IssuerMetadataUtils' +export interface AuthDetails { + type: 'openid_credential' | string + locations?: string | string[] + format: CredentialFormat | CredentialFormat[] + + [s: string]: unknown +} + function getV8CredentialType( offeredCredentialWithMetadata: OfferedCredentialWithMetadata, format: string, diff --git a/packages/openid4vc-holder/src/issuance/utils/IssuerMetadataUtils.ts b/packages/openid4vc-holder/src/issuance/utils/IssuerMetadataUtils.ts index ff35e8372d..1141e198af 100644 --- a/packages/openid4vc-holder/src/issuance/utils/IssuerMetadataUtils.ts +++ b/packages/openid4vc-holder/src/issuance/utils/IssuerMetadataUtils.ts @@ -1,4 +1,4 @@ -import type { AuthDetails } from '../../OpenId4VcHolderServiceOptions' +import type { AuthDetails } from '../OpenId4VciHolderService' import type { CredentialIssuerMetadata, CredentialOfferFormat, From ff1428b78095522388104048358905ca884706bf Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Fri, 10 Nov 2023 17:38:10 +0100 Subject: [PATCH 031/115] chore: basic siop/vp support Signed-off-by: Martin Auer --- .../vc/repository/W3cCredentialRecord.ts | 2 + packages/openid4vc-holder/README.md | 1 + packages/openid4vc-holder/package.json | 5 +- .../src/OpenId4VcHolderApi.ts | 82 +-- .../src/OpenId4VcHolderServiceOptions.ts | 19 + .../src/issuance/OpenId4VciHolderService.ts | 30 +- .../presentations/OpenId4VpHolderService.ts | 492 +++++------------- .../PresentationExchangeService.ts | 385 +++++++------- .../src/presentations/fixtures.ts | 76 --- .../selection/PexCredentialSelection.ts | 201 ++++--- .../presentations/{ => selection}/example.md | 0 .../src/presentations/selection/types.ts | 12 +- packages/openid4vc-holder/src/shared.ts | 86 +++ .../openid4vc-holder/tests/fixtures_vp.ts | 5 + .../tests/openid4vp-holder.e2e.test.ts | 463 ++++++++++++---- packages/openid4vc-verifier/package.json | 2 +- .../src/OpenId4VcVerifierApi.ts | 37 +- .../src/OpenId4VcVerifierService.ts | 272 +++++++++- .../src/OpenId4VcVerifierServiceOptions.ts | 69 ++- packages/openid4vc-verifier/src/index.ts | 8 +- packages/openid4vc-verifier/src/shared.ts | 86 +++ .../src/utils/signatureAlgorithms.ts | 26 + yarn.lock | 58 +-- 23 files changed, 1432 insertions(+), 985 deletions(-) delete mode 100644 packages/openid4vc-holder/src/presentations/fixtures.ts rename packages/openid4vc-holder/src/presentations/{ => selection}/example.md (100%) create mode 100644 packages/openid4vc-holder/src/shared.ts create mode 100644 packages/openid4vc-holder/tests/fixtures_vp.ts create mode 100644 packages/openid4vc-verifier/src/shared.ts create mode 100644 packages/openid4vc-verifier/src/utils/signatureAlgorithms.ts diff --git a/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts b/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts index a5aa1fe070..01171ab68d 100644 --- a/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts +++ b/packages/core/src/modules/vc/repository/W3cCredentialRecord.ts @@ -31,6 +31,7 @@ export type DefaultW3cCredentialTags = { claimFormat: W3cVerifiableCredential['claimFormat'] proofTypes?: Array + types: Array algs?: Array } @@ -64,6 +65,7 @@ export class W3cCredentialRecord extends BaseRecord this.agentContext.wallet.generateNonce())) - const nonce = randomValues[0] + randomValues[1] - const state = randomValues[2] - const correlationId = randomValues[3] - - const authorizationRequest = await relyingParty.createAuthorizationRequest({ - correlationId, - nonce, - state, - }) - - const authorizationRequestUri = await authorizationRequest.uri() - const encodedAuthorizationRequestUri = authorizationRequestUri.encodedUri - - return { - relyingParty, - authorizationRequestUri: encodedAuthorizationRequestUri, - correlationId, - nonce, - state, - } - } - /** * Resolves the authentication request given as URI or JWT to a unified @class VerificationRequest, * then verifies the validity of the request and return the @class VerifiedAuthorizationRequest. + * The resolved request can be accepted with either @see acceptAuthenticationRequest if it is a + * authentication request or with @see acceptPresentationRequest if it is a proofRequest. * @param requestJwtOrUri JWT or an openid:// URI - * @returns the Verified Authorization Request + * @returns the resolved and verified Authorization Request */ - public async resolveRequest(requestOrJwt: string) { - return await this.openId4VpHolderService.resolveAuthorizationRequest(this.agentContext, requestOrJwt) + public async resolveProofRequest(requestOrJwt: string) { + return await this.openId4VpHolderService.resolveProofRequest(this.agentContext, requestOrJwt) } - public async acceptRequest(verifiedRequest: VerifiedAuthorizationRequest, verificationMethod: VerificationMethod) { - const resolved = await this.openId4VpHolderService.acceptRequest( + /** + * Accepts the authentication request after it has been resolved and verified with @see resolveProofRequest. + * @param authenticationRequest - The verified authorization request object. + * @param verificationMethod - The method used for creating the authentication proof. + * @returns @see ProofSubmissionResponse containing the status of the submission. + */ + public async acceptAuthenticationRequest( + authenticationRequest: AuthenticationRequest, + verificationMethod: VerificationMethod + ) { + return await this.openId4VpHolderService.acceptAuthenticationRequest( this.agentContext, - verifiedRequest, - verificationMethod + verificationMethod, + authenticationRequest ) - return resolved + } + + /** + * Accepts the proof request with a presentation after it has been resolved and verified @see resolveProofRequest. + * @param presentationRequest - The verified authorization request object containing the presentation definition. + * @param presentation - An object containing a presentation submission that fulfills the presentation definition. + * @returns @see ProofSubmissionResponse containing the status of the submission. + */ + public async acceptPresentationRequest( + presentationRequest: PresentationRequest, + presentation: { + submission: PresentationSubmission + submissionEntryIndexes: number[] + } + ) { + const { submission, submissionEntryIndexes } = presentation + return await this.openId4VpHolderService.acceptProofRequest(this.agentContext, presentationRequest, { + submission, + submissionEntryIndexes: submissionEntryIndexes, + }) } /** diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts index 9781659143..99179f4152 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts @@ -1,9 +1,28 @@ import type { OfferedCredentialType } from './issuance/utils/IssuerMetadataUtils' +import type { PresentationSubmission, VerifiedAuthorizationRequestWithPresentationDefinition } from './presentations' import type { JwaSignatureAlgorithm, KeyType, VerificationMethod } from '@aries-framework/core' +import type { AuthorizationResponsePayload, VerifiedAuthorizationRequest } from '@sphereon/did-auth-siop' import type { CredentialOfferPayloadV1_0_11, EndpointMetadataResult, OpenId4VCIVersion } from '@sphereon/oid4vci-common' import { OpenIdCredentialFormatProfile } from './issuance/utils/claimFormatMapping' +export type AuthenticationRequest = VerifiedAuthorizationRequest +export type PresentationRequest = VerifiedAuthorizationRequestWithPresentationDefinition + +export type ResolvedProofRequest = + | { proofType: 'authentication'; authenticationRequest: AuthenticationRequest } + | { + proofType: 'presentation' + presentationRequest: PresentationRequest + selectResults: PresentationSubmission + } + +export type ProofSubmissionResponse = { + ok: boolean + status: number + submittedResponse: AuthorizationResponsePayload +} + /** * The credential formats that are supported by the openid4vc holder */ diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts index f890d5b8d7..58464ef897 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts @@ -11,7 +11,6 @@ import type { } from '../OpenId4VcHolderServiceOptions' import type { AgentContext, - CredentialFormat, JwaSignatureAlgorithm, VerificationMethod, W3cVerifiableCredential, @@ -28,6 +27,7 @@ import type { PushedAuthorizationResponse, UniformCredentialOfferPayload, } from '@sphereon/oid4vci-common' +import type { CredentialFormat } from '@sphereon/ssi-types' import { AriesFrameworkError, @@ -45,7 +45,6 @@ import { W3cJsonLdVerifiableCredential, W3cJwtVerifiableCredential, getJwkClassFromJwaSignatureAlgorithm, - getJwkClassFromKeyType, getJwkFromKey, getKeyFromVerificationMethod, getSupportedVerificationMethodTypesFromKeyType, @@ -71,6 +70,7 @@ import { } from '@sphereon/oid4vci-common' import { supportedCredentialFormats } from '../OpenId4VcHolderServiceOptions' +import { getSupportedJwaSignatureAlgorithms } from '../shared' import { OpenIdCredentialFormatProfile, fromOpenIdCredentialFormatProfileToDifClaimFormat } from './utils' import { getFormatForVersion, getUniformFormat } from './utils/Formats' @@ -384,7 +384,7 @@ export class OpenId4VcHolderService { this.logger.info(`Accepting the following credential offers '${credentialsToRequest}'`) const { metadata, issuerMetadata } = await getMetadataFromCredentialOffer(credentialOfferPayload, _metadata) - const supportedJwaSignatureAlgorithms = this.getSupportedJwaSignatureAlgorithms(agentContext) + const supportedJwaSignatureAlgorithms = getSupportedJwaSignatureAlgorithms(agentContext) const possibleProofOfPossessionSigAlgs = acceptCredentialOfferOptions.allowedProofOfPossessionSignatureAlgorithms const allowedProofOfPossessionSignatureAlgorithms = possibleProofOfPossessionSigAlgs @@ -690,29 +690,6 @@ export class OpenId4VcHolderService { } } - /** - * Returns the JWA Signature Algorithms that are supported by the wallet. - * - * This is an approximation based on the supported key types of the wallet. - * This is not 100% correct as a supporting a key type does not mean you support - * all the algorithms for that key type. However, this needs refactoring of the wallet - * that is planned for the 0.5.0 release. - */ - private getSupportedJwaSignatureAlgorithms(agentContext: AgentContext): JwaSignatureAlgorithm[] { - const supportedKeyTypes = agentContext.wallet.supportedKeyTypes - - // Extract the supported JWS algs based on the key types the wallet support. - const supportedJwaSignatureAlgorithms = supportedKeyTypes - // Map the supported key types to the supported JWK class - .map(getJwkClassFromKeyType) - // Filter out the undefined values - .filter((jwkClass): jwkClass is Exclude => jwkClass !== undefined) - // Extract the supported JWA signature algorithms from the JWK class - .flatMap((jwkClass) => jwkClass.supportedSignatureAlgorithms) - - return supportedJwaSignatureAlgorithms - } - private async handleCredentialResponse( agentContext: AgentContext, credentialResponse: OpenIDResponse, @@ -773,7 +750,6 @@ export class OpenId4VcHolderService { const payload = JsonEncoder.toBuffer(jwt.payload) - // TODO: should we support JWK? // We don't support these properties, remove them, so we can pass all other header properties to the JWS service if (jwt.header.x5c || jwt.header.jwk) throw new AriesFrameworkError('x5c and jwk are not supported') diff --git a/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts b/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts index 0184929b1e..6bb697e9f6 100644 --- a/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts +++ b/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts @@ -1,95 +1,44 @@ import type { PresentationSubmission } from './selection' -import type { CredentialsForInputDescriptor } from './selection/types' +import type { InputDescriptorToCredentials } from './selection/types' import type { - AgentContext, - JwaSignatureAlgorithm, - VerificationMethod, - W3cCredentialRecord, - W3cVerifiablePresentation, -} from '@aries-framework/core' -import type { - ClientMetadataOpts, - DIDDocument, - PresentationDefinitionWithLocation, - PresentationVerificationCallback, - URI, - Verification, - VerifiedAuthorizationRequest, -} from '@sphereon/did-auth-siop' -import type { PresentationDefinitionV1 } from '@sphereon/pex-models' + AuthenticationRequest, + PresentationRequest, + ProofSubmissionResponse, + ResolvedProofRequest, +} from '../OpenId4VcHolderServiceOptions' +import type { AgentContext, VerificationMethod, W3cVerifiablePresentation } from '@aries-framework/core' +import type { PresentationDefinitionWithLocation, VerifiedAuthorizationRequest } from '@sphereon/did-auth-siop' import type { W3CVerifiablePresentation } from '@sphereon/ssi-types' import { AriesFrameworkError, DidsApi, - getJwkClassFromKeyType, - getKeyFromVerificationMethod, injectable, - TypedArrayEncoder, W3cJsonLdVerifiablePresentation, asArray, inject, InjectionSymbols, Logger, - JwsService, } from '@aries-framework/core' import { + CheckLinkedDomain, OP, ResponseIss, ResponseMode, SupportedVersion, + VPTokenLocation, VerificationMode, - CheckLinkedDomain, - RP, - SigningAlgo, - RevocationVerification, - ResponseType, - Scope, - SubjectType, - PassBy, } from '@sphereon/did-auth-siop' -import { PresentationExchangeService } from './PresentationExchangeService' - -export const staticOpSiopConfig: ClientMetadataOpts & { authorization_endpoint: string } = { - authorization_endpoint: 'siopv2:', - subject_syntax_types_supported: ['urn:ietf:params:oauth:jwk-thumbprint'], - responseTypesSupported: [ResponseType.ID_TOKEN], - scopesSupported: [Scope.OPENID], - subjectTypesSupported: [SubjectType.PAIRWISE], - idTokenSigningAlgValuesSupported: [SigningAlgo.ES256], - requestObjectSigningAlgValuesSupported: [SigningAlgo.ES256], - passBy: PassBy.VALUE, -} +import { getResolver, getSuppliedSignatureFromVerificationMethod, getSupportedDidMethods } from '../shared' -export const staticOpOpenIdConfig: ClientMetadataOpts & { authorization_endpoint: string } = { - authorization_endpoint: 'openid:', - subject_syntax_types_supported: ['urn:ietf:params:oauth:jwk-thumbprint'], - responseTypesSupported: [ResponseType.ID_TOKEN, ResponseType.VP_TOKEN], - scopesSupported: [Scope.OPENID], - subjectTypesSupported: [SubjectType.PAIRWISE], - idTokenSigningAlgValuesSupported: [SigningAlgo.ES256], - requestObjectSigningAlgValuesSupported: [SigningAlgo.ES256], - passBy: PassBy.VALUE, - vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.ES256] }, jwt_vp: { alg: [SigningAlgo.ES256] } }, -} - -export function getSupportedDidMethods(agentContext: AgentContext) { - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - const supportedDidMethods: Set = new Set() - - for (const resolver of didsApi.config.resolvers) { - resolver.supportedMethods.forEach((method) => supportedDidMethods.add(method)) - } - - return Array.from(supportedDidMethods) -} +import { PresentationExchangeService } from './PresentationExchangeService' /** - * SIOPv2 Authorization Request with a single v1 presentation definition + * SIOPv2 Authorization Request with a single v1 / v2 presentation definition */ export type VerifiedAuthorizationRequestWithPresentationDefinition = VerifiedAuthorizationRequest & { - presentationDefinitions: [PresentationDefinitionWithLocation & { definition: PresentationDefinitionV1 }] + presentationDefinitions: [PresentationDefinitionWithLocation] } function isVerifiedAuthorizationRequestWithPresentationDefinition( @@ -102,234 +51,37 @@ function isVerifiedAuthorizationRequestWithPresentationDefinition( ) } -// TODO: duplicate -/** - * Returns the JWA Signature Algorithms that are supported by the wallet. - * - * This is an approximation based on the supported key types of the wallet. - * This is not 100% correct as a supporting a key type does not mean you support - * all the algorithms for that key type. However, this needs refactoring of the wallet - * that is planned for the 0.5.0 release. - */ -function getSupportedJwaSignatureAlgorithms(agentContext: AgentContext): JwaSignatureAlgorithm[] { - const supportedKeyTypes = agentContext.wallet.supportedKeyTypes - - // Extract the supported JWS algs based on the key types the wallet support. - const supportedJwaSignatureAlgorithms = supportedKeyTypes - // Map the supported key types to the supported JWK class - .map(getJwkClassFromKeyType) - // Filter out the undefined values - .filter((jwkClass): jwkClass is Exclude => jwkClass !== undefined) - // Extract the supported JWA signature algorithms from the JWK class - .flatMap((jwkClass) => jwkClass.supportedSignatureAlgorithms) - - return supportedJwaSignatureAlgorithms -} - -export function getResolver(agentContext: AgentContext) { - return { - resolve: async (didUrl: string) => { - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - const result = await didsApi.resolve(didUrl) - - return { - ...result, - didDocument: result.didDocument?.toJSON() as DIDDocument, - } - }, - } -} - @injectable() export class OpenId4VpHolderService { private logger: Logger - private jwsService: JwsService public constructor( @inject(InjectionSymbols.Logger) logger: Logger, - jwsService: JwsService, private presentationExchangeService: PresentationExchangeService ) { - this.jwsService = jwsService this.logger = logger } - public async getRelyingParty( - agentContext: AgentContext, - options: { - verificationMethod: VerificationMethod | ((clientMetadata: ClientMetadataOpts) => VerificationMethod) - clientMetadata?: ClientMetadataOpts & { authorization_endpoint?: string } - issuer?: string - redirect_url: string - } - ) { - const { verificationMethod: _verificationMethod, issuer, redirect_url } = options - - const supportedDidMethods = getSupportedDidMethods(agentContext) - - // authorization_endpoint - // TODO: - const isVpRequest = false - - let clientMetadata: ClientMetadataOpts & { authorization_endpoint?: string } - if (options.clientMetadata) { - // use the provided client metadata - clientMetadata = options.clientMetadata - } else if (issuer) { - // Use OpenId Discovery to get the client metadata - let reference_uri = issuer - if (!issuer.endsWith('/.well-known/openid-configuration')) { - reference_uri = issuer + '/.well-known/openid-configuration' - } - clientMetadata = { reference_uri, passBy: PassBy.REFERENCE } - } else if (isVpRequest) { - // if neither clientMetadata nor issuer is provided, use a static config - clientMetadata = staticOpOpenIdConfig - } else { - // if neither clientMetadata nor issuer is provided, use a static config - clientMetadata = staticOpSiopConfig - } - - let verificationMethod: VerificationMethod - if (typeof _verificationMethod === 'function') { - verificationMethod = _verificationMethod(clientMetadata) - } else { - verificationMethod = _verificationMethod - } - - const { signature, did, kid, alg } = await this.getSuppliedSignatureFromVerificationMethod( - agentContext, - verificationMethod - ) - - // Check if the OpenId Provider (Holder) can validate the request signature provided by the Relying Party (Verifier) - const requestObjectSigningAlgValuesSupported = clientMetadata.requestObjectSigningAlgValuesSupported - if (requestObjectSigningAlgValuesSupported && !requestObjectSigningAlgValuesSupported.includes(alg)) { - throw new AriesFrameworkError( - [ - `Cannot sign authorization request with '${alg}' that isn't supported by the OpenId Provider.`, - `Supported algorithms are ${requestObjectSigningAlgValuesSupported}`, - ].join('\n') - ) - } - - // Check if the Relying Party (Verifier) can validate the IdToken provided by the OpenId Provider (Holder) - const idTokenSigningAlgValuesSupported = clientMetadata.idTokenSigningAlgValuesSupported - const rpSupportedSignatureAlgorithms = getSupportedJwaSignatureAlgorithms(agentContext) as unknown as SigningAlgo[] - - if (idTokenSigningAlgValuesSupported) { - const possibleIdTokenSigningAlgValues = Array.isArray(idTokenSigningAlgValuesSupported) - ? idTokenSigningAlgValuesSupported.filter((value) => rpSupportedSignatureAlgorithms.includes(value)) - : [idTokenSigningAlgValuesSupported].filter((value) => rpSupportedSignatureAlgorithms.includes(value)) - - if (!possibleIdTokenSigningAlgValues) { - throw new AriesFrameworkError( - [ - `The OpenId Provider supports no signature algorithms that are supported by the Relying Party.`, - `Relying Party supported algorithms are ${rpSupportedSignatureAlgorithms}.`, - `OpenId Provider supported algorithms are ${idTokenSigningAlgValuesSupported}.`, - ].join('\n') - ) - } - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const presentationVerificationCallback: PresentationVerificationCallback = async (x) => { - // TODO: - this.logger.info('verifying presentation') - return Promise.resolve({ - verified: true, - }) - } - - const builder = RP.builder() - // .withClientId('client-id') // The client-id is set to the relying party did - .withRedirectUri(redirect_url) - .withRequestByValue() - .withPresentationVerification(presentationVerificationCallback) - .withIssuer(ResponseIss.SELF_ISSUED_V2) // TODO: this must be set to the issuer with dynamic disc // REQUIRED. URL using the https scheme with no query or fragment component that the Self-Issued OP asserts as its Issuer Identifier. MUST be identical to the iss Claim value in ID Tokens issued from this Self-Issued OP. - .withSuppliedSignature(signature, did, kid, alg) - .withRevocationVerification(RevocationVerification.NEVER) - .withSupportedVersions(SupportedVersion.SIOPv2_D11) - .withClientMetadata(clientMetadata) - .withCustomResolver(getResolver(agentContext)) - .withResponseMode(ResponseMode.POST) - .withResponseType(isVpRequest ? ResponseType.VP_TOKEN : ResponseType.ID_TOKEN) - // .withCheckLinkedDomain - // .withEventEmitter - // .withPresentationDefinition - // .withPresentationVerification - // .withSessionManager - - if (clientMetadata.authorizationEndpoint) { - builder.withAuthorizationEndpoint(clientMetadata.authorizationEndpoint) - } - - for (const supportedDidMethod of supportedDidMethods) { - builder.addDidMethod(supportedDidMethod) - } - - return builder.build() - } - - private async getSuppliedSignatureFromVerificationMethod( - agentContext: AgentContext, - verificationMethod: VerificationMethod - ) { - // get the key from the verification method and use the first supported signature algorithm - const key = getKeyFromVerificationMethod(verificationMethod) - const alg = getJwkClassFromKeyType(key.keyType)?.supportedSignatureAlgorithms[0] - if (!alg) throw new AriesFrameworkError(`No supported signature algorithms for key type: ${key.keyType}`) - - const suppliedSignature = { - signature: async (data: string | Uint8Array) => { - if (typeof data === 'string') { - const signedData = await agentContext.wallet.sign({ - data: typeof data === 'string' ? TypedArrayEncoder.fromString(data) : data, - key, - }) - - const signature = TypedArrayEncoder.toBase64URL(signedData) - - return signature - } - throw new AriesFrameworkError('TODO: this should not hjappen') - }, - // FIXME: cast - alg: alg as unknown as SigningAlgo, - did: verificationMethod.controller, - kid: verificationMethod.id, - } - - return suppliedSignature - } - private async getOpenIdProvider( agentContext: AgentContext, options: { - verificationMethod?: VerificationMethod | (() => VerificationMethod) + verificationMethod?: VerificationMethod } ) { - const { verificationMethod: _verificationMethod } = options + const { verificationMethod } = options const builder = OP.builder() .withExpiresIn(6000) - // TODO: .withIssuer(ResponseIss.SELF_ISSUED_V2) .withResponseMode(ResponseMode.POST) - .withSupportedVersions([SupportedVersion.SIOPv2_D11]) - .withCheckLinkedDomain(CheckLinkedDomain.NEVER) + .withSupportedVersions([SupportedVersion.SIOPv2_D11, SupportedVersion.SIOPv2_D12_OID4VP_D18]) .withCustomResolver(getResolver(agentContext)) + .withCheckLinkedDomain(CheckLinkedDomain.NEVER) + // .withPresentationSignCallback + // .withEventEmitter + // .withRegistration() - let verificationMethod: VerificationMethod - - if (_verificationMethod) { - if (typeof _verificationMethod === 'function') { - verificationMethod = _verificationMethod() - } else { - verificationMethod = _verificationMethod - } - - const { signature, did, kid, alg } = await this.getSuppliedSignatureFromVerificationMethod( + if (verificationMethod) { + const { signature, did, kid, alg } = await getSuppliedSignatureFromVerificationMethod( agentContext, verificationMethod ) @@ -343,96 +95,81 @@ export class OpenId4VpHolderService { builder.addDidMethod(supportedDidMethod) } - const op = builder.build() + const openidProvider = builder.build() - return op + return openidProvider } - public async resolveAuthorizationRequest( - agentContext: AgentContext, - requestJwtOrUri: string, - options?: { - correlationId: string - } - ) { - const { correlationId } = options ?? {} - const op = await this.getOpenIdProvider(agentContext, {}) + public async resolveProofRequest(agentContext: AgentContext, requestJwtOrUri: string): Promise { + const openidProvider = await this.getOpenIdProvider(agentContext, {}) // parsing happens automatically in verifyAuthorizationRequest - const verifiedRequest = await op.verifyAuthorizationRequest(requestJwtOrUri, { - // correlationId, - //verification: { - //mode: VerificationMode.EXTERNAL, - //resolveOpts: { resolver: getResolver(agentContext), noUniversalResolverFallback: false }, - //}, + const verifiedAuthorizationRequest = await openidProvider.verifyAuthorizationRequest(requestJwtOrUri, { + verification: { + mode: VerificationMode.INTERNAL, + resolveOpts: { resolver: getResolver(agentContext), noUniversalResolverFallback: false }, + }, }) - this.logger.debug(`verified SIOP Authorization Request for issuer '${verifiedRequest.issuer}'`) + this.logger.debug(`verified SIOP Authorization Request for issuer '${verifiedAuthorizationRequest.issuer}'`) this.logger.debug(`requestJwtOrUri '${requestJwtOrUri}'`) - const presentationDefs = verifiedRequest.presentationDefinitions - if (presentationDefs !== undefined && presentationDefs.length > 0) { - throw new AriesFrameworkError('Not supported yet') - } - - return verifiedRequest - } - - public async acceptRequest( - agentContext: AgentContext, - verifiedReq: VerifiedAuthorizationRequest, - verificationMethod: VerificationMethod - ) { - const op = await this.getOpenIdProvider(agentContext, { verificationMethod }) - - const suppliedSignature = await this.getSuppliedSignatureFromVerificationMethod(agentContext, verificationMethod) - - // TODO: presentations - const authRespWithJWT = await op.createAuthorizationResponse(verifiedReq, { - signature: suppliedSignature, - // https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-aud-of-a-request-object - audience: 'https://acme.com/hello', - }) - return authRespWithJWT - //const response = await op.submitAuthorizationResponse(authRespWithJWT) - //return response - } - - public async selectCredentialForProofRequest( - agentContext: AgentContext, - options: { - authorizationRequest: string | URI + // If the presentationDefinitions array property is present it means the op.verifyAuthorizationRequest + // already has established that the Presentation Definition(s) itself were valid and present. + // It has populated the presentationDefinitions array for you. + // If the definition was not valid, the verify method would have thrown an error, + // which means you should never continue the authentication flow! + const presentationDefs = verifiedAuthorizationRequest.presentationDefinitions + if (!presentationDefs || presentationDefs.length === 0) { + return { proofType: 'authentication', authenticationRequest: verifiedAuthorizationRequest } } - ) { - const op = await this.getOpenIdProvider(agentContext, {}) - - const verification = { - mode: VerificationMode.EXTERNAL, - resolveOpts: { - resolver: getResolver(agentContext), - noUniversalResolverFallback: true, - }, - } satisfies Verification - - // FIXME: this uses did-jwt for verification of the JWT, we can't verify it ourselves. - const verifiedAuthorizationRequest = await op.verifyAuthorizationRequest(options.authorizationRequest, { - verification, - }) + // FIXME: I don't see any reason why we would support multiple presentation definitions + // but the library does support it. For now we only support a single presentation definition. if (!isVerifiedAuthorizationRequestWithPresentationDefinition(verifiedAuthorizationRequest)) { throw new AriesFrameworkError( - 'Only SIOPv2 authorization request including a single presentation definition are supported' + 'Only SIOPv2 authorization request including a single presentation definition are supported.' ) } + const presentationDefinition = verifiedAuthorizationRequest.presentationDefinitions[0].definition + const selectResults = await this.presentationExchangeService.selectCredentialsForRequest( agentContext, - verifiedAuthorizationRequest.presentationDefinitions[0].definition + presentationDefinition + ) + + return { proofType: 'presentation', presentationRequest: verifiedAuthorizationRequest, selectResults } + } + + /** + * Send a SIOPv2 authentication response to the relying party including a verifiable + * presentation based on OpenID4VP. + */ + public async acceptAuthenticationRequest( + agentContext: AgentContext, + verificationMethod: VerificationMethod, + authenticationRequest: AuthenticationRequest + ): Promise { + const openidProvider = await this.getOpenIdProvider(agentContext, { verificationMethod }) + + const suppliedSignature = await getSuppliedSignatureFromVerificationMethod(agentContext, verificationMethod) + + const authorizationResponseWithCorrelationId = await openidProvider.createAuthorizationResponse( + authenticationRequest, + { + signature: suppliedSignature, + issuer: verificationMethod.controller, + // https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-aud-of-a-request-object + audience: authenticationRequest.authorizationRequestPayload.client_id, + } ) + const response = await openidProvider.submitAuthorizationResponse(authorizationResponseWithCorrelationId) return { - verifiedAuthorizationRequest, - selectResults, + ok: response.status === 200, + status: response.status, + submittedResponse: authorizationResponseWithCorrelationId.response.payload, } } @@ -440,28 +177,26 @@ export class OpenId4VpHolderService { * Send a SIOPv2 authentication response to the relying party including a verifiable * presentation based on OpenID4VP. */ - public async shareProof( + public async acceptProofRequest( agentContext: AgentContext, + presentationRequest: PresentationRequest, options: { - verifiedAuthorizationRequest: VerifiedAuthorizationRequestWithPresentationDefinition submission: PresentationSubmission submissionEntryIndexes: number[] } - ) { - const op = await this.getOpenIdProvider(agentContext, {}) + ): Promise { + const { submission, submissionEntryIndexes } = options - const credentialsForInputDescriptor: CredentialsForInputDescriptor = {} + const credentialsForInputDescriptor: InputDescriptorToCredentials = {} - options.submission.requirements - .flatMap((requirement) => requirement.submission) - .forEach((submission, index) => { - const verifiableCredential = submission.verifiableCredentials[ - options.submissionEntryIndexes[index] as number - ] as W3cCredentialRecord + submission.requirements + .flatMap((requirement) => requirement.submissionEntry) + .forEach((submissionEntry, index) => { + const verifiableCredential = submissionEntry.verifiableCredentials[submissionEntryIndexes[index]] - const inputDescriptor = credentialsForInputDescriptor[submission.inputDescriptorId] + const inputDescriptor = credentialsForInputDescriptor[submissionEntry.inputDescriptorId] if (!inputDescriptor) { - credentialsForInputDescriptor[submission.inputDescriptorId] = [verifiableCredential.credential] + credentialsForInputDescriptor[submissionEntry.inputDescriptorId] = [verifiableCredential.credential] } else { inputDescriptor.push(verifiableCredential.credential) } @@ -470,10 +205,8 @@ export class OpenId4VpHolderService { const { verifiablePresentations, presentationSubmission } = await this.presentationExchangeService.createPresentation(agentContext, { credentialsForInputDescriptor, - presentationDefinition: options.verifiedAuthorizationRequest.presentationDefinitions[0].definition, - includePresentationSubmissionInVp: false, - // TODO: are there other properties we need to include? - nonce: await options.verifiedAuthorizationRequest.authorizationRequest.getMergedProperty('nonce'), + presentationDefinition: presentationRequest.presentationDefinitions[0].definition, + nonce: await presentationRequest.authorizationRequest.getMergedProperty('nonce'), }) const verificationMethod = await this.getVerificationMethodFromVerifiablePresentation( @@ -481,23 +214,34 @@ export class OpenId4VpHolderService { verifiablePresentations[0] as W3cVerifiablePresentation ) - const response = await op.createAuthorizationResponse(options.verifiedAuthorizationRequest, { - issuer: verificationMethod.controller, - presentationExchange: { - verifiablePresentations: verifiablePresentations.map((vp) => vp.encoded as W3CVerifiablePresentation), - presentationSubmission, - }, - signature: await this.getSuppliedSignatureFromVerificationMethod(agentContext, verificationMethod), - }) + const openidProvider = await this.getOpenIdProvider(agentContext, { verificationMethod }) - const responseToResponse = await op.submitAuthorizationResponse(response) + const suppliedSignature = await getSuppliedSignatureFromVerificationMethod(agentContext, verificationMethod) - if (!responseToResponse.ok) { - throw new AriesFrameworkError(`Error submitting authorization response. ${await responseToResponse.text()}`) + const authorizationResponseWithCorrelationId = await openidProvider.createAuthorizationResponse( + presentationRequest, + { + signature: suppliedSignature, + issuer: verificationMethod.controller, + // https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-aud-of-a-request-object + audience: presentationRequest.authorizationRequestPayload.client_id, + + presentationExchange: { + verifiablePresentations: verifiablePresentations.map((vp) => vp.encoded as W3CVerifiablePresentation), + presentationSubmission, + vpTokenLocation: VPTokenLocation.AUTHORIZATION_RESPONSE, + }, + } + ) + + const response = await openidProvider.submitAuthorizationResponse(authorizationResponseWithCorrelationId) + return { + ok: response.status >= 200 && response.status < 300, + status: response.status, + submittedResponse: authorizationResponseWithCorrelationId.response.payload, } } - // TODO: we can do this in a simpler way, as we're now resolving it multiple times private async getVerificationMethodFromVerifiablePresentation( agentContext: AgentContext, verifiablePresentation: W3cVerifiablePresentation @@ -507,18 +251,16 @@ export class OpenId4VpHolderService { let verificationMethodId: string if (verifiablePresentation instanceof W3cJsonLdVerifiablePresentation) { const [firstProof] = asArray(verifiablePresentation.proof) + if (!firstProof) throw new AriesFrameworkError('Verifiable presentation does not contain a proof') - if (!firstProof) { - throw new AriesFrameworkError('Verifiable presentation does not contain a proof') - } verificationMethodId = firstProof.verificationMethod } else { - // FIXME: cast - verificationMethodId = verifiablePresentation.jwt.header.kid as string + const kid = verifiablePresentation.jwt.header.kid + if (!kid) throw new AriesFrameworkError('Verifiable Presentation does not contain a kid in the jwt header') + verificationMethodId = kid } const didDocument = await didsApi.resolveDidDocument(verificationMethodId) - return didDocument.dereferenceKey(verificationMethodId, ['authentication']) } } diff --git a/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts b/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts index f5ec68323e..f7fc2e1575 100644 --- a/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts +++ b/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts @@ -1,19 +1,22 @@ -import type { CredentialsForInputDescriptor, PresentationSubmission } from './selection/types' +import type { InputDescriptorToCredentials, PresentationSubmission } from './selection/types' import type { AgentContext, Query, VerificationMethod, W3cCredentialRecord, W3cVerifiableCredential, + W3cVerifiablePresentation, } from '@aries-framework/core' -import type { PresentationSignCallBackParams, VerifiablePresentationResult } from '@sphereon/pex' +import type { + IPresentationDefinition, + PresentationSignCallBackParams, + VerifiablePresentationResult, +} from '@sphereon/pex' import type { PresentationDefinitionV1, - PresentationDefinitionV2, PresentationSubmission as PexPresentationSubmission, Descriptor, } from '@sphereon/pex-models' -import type { IVerifiablePresentation } from '@sphereon/ssi-types' import { AriesFrameworkError, @@ -27,7 +30,7 @@ import { W3cPresentation, W3cCredentialRepository, } from '@aries-framework/core' -import { PEVersion, PEX, Status } from '@sphereon/pex' +import { PEVersion, PEX } from '@sphereon/pex' import { selectCredentialsForRequest } from './selection/PexCredentialSelection' import { @@ -36,110 +39,143 @@ import { getW3cVerifiablePresentationInstance, } from './transform' +type ProofStructure = { + [subjectId: string]: { + [inputDescriptorId: string]: W3cVerifiableCredential[] + } +} + @injectable() export class PresentationExchangeService { private pex = new PEX() + public async selectCredentialsForRequest( + agentContext: AgentContext, + presentationDefinition: IPresentationDefinition + ): Promise { + const credentialRecords = await this.queryCredentialForPresentationDefinition(agentContext, presentationDefinition) + + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const didRecords = await didsApi.getCreatedDids() + const holderDIDs = didRecords.map((didRecord) => didRecord.did) + + return selectCredentialsForRequest(presentationDefinition, credentialRecords, holderDIDs) + } + /** - * Validates a DIF Presentation Definition + * Queries the wallet for credentials that match the given presentation definition. This only does an initial query based on the + * schema of the input descriptors. It does not do any further filtering based on the constraints in the input descriptors. */ - public validateDefinition(presentationDefinition: PresentationDefinitionV1) { - const result = PEX.validateDefinition(presentationDefinition) + private async queryCredentialForPresentationDefinition( + agentContext: AgentContext, + presentationDefinition: IPresentationDefinition + ) { + const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + const query: Array> = [] + const presentationDefinitionVersion = PEX.definitionVersionDiscovery(presentationDefinition) + + if (!presentationDefinitionVersion.version) { + throw new AriesFrameworkError( + `Unable to determine the Presentation Exchange version from the presentation definition. ${ + presentationDefinitionVersion.error ?? 'Unknown error' + }` + ) + } - // check if error - const firstResult = Array.isArray(result) ? result[0] : result + if (presentationDefinitionVersion.version === PEVersion.v1) { + const pd = presentationDefinition as PresentationDefinitionV1 - if (firstResult.status !== Status.INFO) { + // The schema.uri can contain either an expanded type, or a context uri + for (const inputDescriptor of pd.input_descriptors) { + for (const schema of inputDescriptor.schema) { + // TODO: write migration + query.push({ + $or: [{ expandedType: [schema.uri] }, { contexts: [schema.uri] }, { type: [schema.uri] }], + }) + } + } + } else if (presentationDefinitionVersion.version === PEVersion.v2) { + // FIXME: As PE version 2 does not have the `schema` anymore, we can't query by schema anymore. + // For now we retrieve ALL credentials, as we did the same for V1 with JWT credentials. We probably need + // to find some way to do initial filtering, hopefully if there's a filter on the `type` field or something. + } else { throw new AriesFrameworkError( - `Error in presentation exchange presentationDefinition: ${firstResult?.message ?? 'Unknown'} ` + `Unsupported presentation definition version ${presentationDefinitionVersion.version as unknown as string}` ) } - } - public evaluatePresentation({ - presentationDefinition, - presentation, - }: { - presentationDefinition: PresentationDefinitionV1 - presentation: IVerifiablePresentation - }) { - // validate contents of presentation - const evaluationResults = this.pex.evaluatePresentation(presentationDefinition, presentation) - - return evaluationResults + // query the wallet ourselves first to avoid the need to query the pex library for all + // credentials for every proof request + const credentialRecords = await w3cCredentialRepository.findByQuery(agentContext, { + $or: query, + }) + + return credentialRecords } - public async selectCredentialsForRequest( - agentContext: AgentContext, - presentationDefinition: PresentationDefinitionV1 - ): Promise { - const credentialRecords = await this.queryCredentialForPresentationDefinition(agentContext, presentationDefinition) + private addCredentialForSubjectWithInputDescriptorId( + subjectsToInputDescriptors: ProofStructure, + subjectId: string, + inputDescriptorId: string, + credential: W3cVerifiableCredential + ) { + const inputDescriptorsToCredentials = subjectsToInputDescriptors[subjectId] ?? {} + const credentials = inputDescriptorsToCredentials[inputDescriptorId] ?? [] - return selectCredentialsForRequest(presentationDefinition, credentialRecords) + credentials.push(credential) + inputDescriptorsToCredentials[inputDescriptorId] = credentials + subjectsToInputDescriptors[subjectId] = inputDescriptorsToCredentials } public async createPresentation( agentContext: AgentContext, - { - credentialsForInputDescriptor, - presentationDefinition, - challenge, - domain, - nonce, - includePresentationSubmissionInVp = true, - }: { - credentialsForInputDescriptor: CredentialsForInputDescriptor - presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2 + options: { + credentialsForInputDescriptor: InputDescriptorToCredentials + presentationDefinition: IPresentationDefinition challenge?: string domain?: string nonce?: string - includePresentationSubmissionInVp?: boolean } ) { - const vps: { - [subjectId: string]: { - [inputDescriptorId: string]: W3cVerifiableCredential[] - } - } = {} + const { presentationDefinition, challenge, nonce, domain } = options - const verifiablePresentationResults: VerifiablePresentationResult[] = [] + const proofStructure: ProofStructure = {} - Object.entries(credentialsForInputDescriptor).forEach(([inputDescriptorId, credentials]) => { + Object.entries(options.credentialsForInputDescriptor).forEach(([inputDescriptorId, credentials]) => { credentials.forEach((credential) => { - const firstCredentialSubjectId = credential.credentialSubjectIds[0] - if (!firstCredentialSubjectId) { - throw new AriesFrameworkError( - 'Credential subject missing from the selected credential for creating presentation.' - ) + const subjectId = credential.credentialSubjectIds[0] + if (!subjectId) { + throw new AriesFrameworkError('Missing required credential subject for creating the presentation.') } - const inputDescriptorsForSubject = vps[firstCredentialSubjectId] ?? {} - vps[firstCredentialSubjectId] = inputDescriptorsForSubject - - const credentialsForInputDescriptor = inputDescriptorsForSubject[inputDescriptorId] ?? [] - inputDescriptorsForSubject[inputDescriptorId] = credentialsForInputDescriptor - - credentialsForInputDescriptor.push(credential) + this.addCredentialForSubjectWithInputDescriptorId(proofStructure, subjectId, inputDescriptorId, credential) }) }) - for (const [subjectId, inputDescriptors] of Object.entries(vps)) { + const verifiablePresentationResults: VerifiablePresentationResult[] = [] + + const subjectToInputDescriptors = Object.entries(proofStructure) + for (const [subjectId, inputDescriptorsToCredentials] of subjectToInputDescriptors) { // Determine a suitable verification method for the presentation const verificationMethod = await this.getVerificationMethodForSubjectId(agentContext, subjectId) if (!verificationMethod) { - throw new AriesFrameworkError(`No verification method found for subject id ${subjectId}`) + throw new AriesFrameworkError(`No verification method found for subject id '${subjectId}'.`) } - const inputDescriptorsForVp = (presentationDefinition.input_descriptors as PresentationDefinitionV1[]).filter( - (inputDescriptor) => inputDescriptor.id in inputDescriptors + // We create a presentation for each subject + // Thus for each subject we need to filter all the related input descriptors and credentials + // FIXME: cast to V1, as tsc errors for strange reasons if not + const inputDescriptorsForVp = (presentationDefinition as PresentationDefinitionV1).input_descriptors.filter( + (inputDescriptor) => inputDescriptor.id in inputDescriptorsToCredentials ) - const credentialsForVp = Object.values(inputDescriptors) + // Get all the credentials associated with the input descriptors + const credentialsForVp = Object.values(inputDescriptorsToCredentials) .flatMap((inputDescriptors) => inputDescriptors) .map(getSphereonW3cVerifiableCredential) - const presentationDefinitionForVp = { + const presentationDefinitionForVp: IPresentationDefinition = { ...presentationDefinition, input_descriptors: inputDescriptorsForVp, @@ -148,41 +184,30 @@ export class PresentationExchangeService { submission_requirements: undefined, } - // Q1: is holder always subject id, what if there are multiple subjects??? - // Q2: What about proofType, proofPurpose verification method for multiple subjects? + // FIXME: Q1: is holder always subject id, what if there are multiple subjects??? + // FIXME: Q2: What about proofType, proofPurpose verification method for multiple subjects? const verifiablePresentationResult = await this.pex.verifiablePresentationFrom( presentationDefinitionForVp, credentialsForVp, - this.getPresentationSignCallback( - agentContext, - verificationMethod, - // Can't include submission if more than one VP - Object.values(vps).length > 1 ? false : includePresentationSubmissionInVp - ), + this.getPresentationSignCallback(agentContext, verificationMethod), { holderDID: subjectId, - proofOptions: { - challenge, - domain, - nonce, - }, - signatureOptions: { - verificationMethod: verificationMethod?.id, - }, + proofOptions: { challenge, domain, nonce }, + signatureOptions: { verificationMethod: verificationMethod?.id }, } ) verifiablePresentationResults.push(verifiablePresentationResult) } - const firstVerifiablePresentationResult = verifiablePresentationResults[0] - if (!firstVerifiablePresentationResult) { - throw new AriesFrameworkError('No verifiable presentations created.') + if (subjectToInputDescriptors.length !== verifiablePresentationResults.length) { + if (!verifiablePresentationResults[0]) throw new AriesFrameworkError('No verifiable presentations created.') + throw new AriesFrameworkError('Invalid amount of verifiable presentations created.') } const presentationSubmission: PexPresentationSubmission = { - id: firstVerifiablePresentationResult.presentationSubmission.id, - definition_id: firstVerifiablePresentationResult.presentationSubmission.definition_id, + id: verifiablePresentationResults[0].presentationSubmission.id, + definition_id: verifiablePresentationResults[0].presentationSubmission.definition_id, descriptor_map: [], } @@ -211,64 +236,116 @@ export class PresentationExchangeService { getW3cVerifiablePresentationInstance(r.verifiablePresentation) ), presentationSubmission, - presentationSubmissionLocation: firstVerifiablePresentationResult.presentationSubmissionLocation, + presentationSubmissionLocation: verifiablePresentationResults[0].presentationSubmissionLocation, } } - public getPresentationSignCallback( - agentContext: AgentContext, + private getSigningAlgorithmFromVerificationMethod( verificationMethod: VerificationMethod, - includePresentationSubmissionInVp = true + suitableAlgorithms?: string[] + ) { + const key = getKeyFromVerificationMethod(verificationMethod) + const jwk = getJwkFromKey(key) + + if (suitableAlgorithms) { + const possibleAlgorithms = jwk.supportedSignatureAlgorithms.filter((alg) => suitableAlgorithms?.includes(alg)) + if (!possibleAlgorithms || possibleAlgorithms.length === 0) { + throw new AriesFrameworkError( + [ + `Found no suitable signing algorithm.`, + `Algorithms supported by Verification method: ${jwk.supportedSignatureAlgorithms.join(', ')}`, + `Suitable algorithms: ${suitableAlgorithms.join(', ')}`, + ].join('\n') + ) + } + } + + const alg = jwk.supportedSignatureAlgorithms[0] + if (!alg) throw new AriesFrameworkError(`No supported algs for key type: ${key.keyType}`) + return alg + } + + private getSigningAlgorithmForJwtVc( + presentationDefinition: IPresentationDefinition, + verificationMethod: VerificationMethod + ) { + const suitableAlgorithms = presentationDefinition.format?.jwt_vc?.alg + // const inputDescriptors: InputDescriptorV2[] = presentationDefinition.input_descriptors as InputDescriptorV2[] + + // TODO: continue + + // const inputDescriptorAlgorithms: string[][] = inputDescriptors + // .map((inputDescriptor) => inputDescriptor.format?.jwt_vc?.alg) + // .filter((alg): alg is string[] => alg !== undefined && alg.length === 0) + + // const allInputDescriptorAlgorithms = inputDescriptorAlgorithms.flat() + + // const isAlgInEveryInputDescriptor = inputDescriptorAlgorithms.every((alg, _, arr) => arr.includes(alg)) + + return this.getSigningAlgorithmFromVerificationMethod(verificationMethod, suitableAlgorithms) + } + + private getSigningAlgorithmForLdpVc( + presentationDefinition: IPresentationDefinition, + verificationMethod: VerificationMethod ) { + const suitableSignaturesSuites = presentationDefinition.format?.ldp_vc?.proof_type + // TODO: find out which signature suites are supported by the verification method + // TODO: check if a supported signature suite is in the list of suitable signature suites + // TODO: remake this after + if (!suitableSignaturesSuites || suitableSignaturesSuites.length === 0) + throw new AriesFrameworkError(`No suitable signature suite found for presentation definition.`) + + return suitableSignaturesSuites[0] + } + + public getPresentationSignCallback(agentContext: AgentContext, verificationMethod: VerificationMethod) { const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) return async (callBackParams: PresentationSignCallBackParams) => { // The created partial proof and presentation, as well as original supplied options - const { presentation: presentationJson, options } = callBackParams + const { presentation: presentationJson, options, presentationDefinition } = callBackParams const { challenge, domain, nonce } = options.proofOptions ?? {} const { verificationMethod: verificationMethodId } = options.signatureOptions ?? {} - let presentationToSignJson = presentationJson - if (!includePresentationSubmissionInVp) { - presentationToSignJson = { - ...presentationToSignJson, - presentation_submission: undefined, - } - } - const w3cPresentation = JsonTransformer.fromJSON(presentationToSignJson, W3cPresentation) - if (verificationMethodId && verificationMethodId !== verificationMethod.id) { throw new AriesFrameworkError( `Verification method from signing options ${verificationMethodId} does not match verification method ${verificationMethod.id}.` ) } - // NOTE: we currently don't support mixed presentations, where some credentials - // are JWT and some are JSON-LD. It could be however that the presentation contains - // some JWT and some JSON-LD credentials. (for DDIP we only support JWT, so we should be fine) - const isJwt = typeof presentationJson.verifiableCredential?.[0] === 'string' - - if (!isJwt) { - throw new AriesFrameworkError(`Only JWT credentials are supported for presentation exchange.`) - } - - const key = getKeyFromVerificationMethod(verificationMethod) - const jwk = getJwkFromKey(key) - - const alg = jwk.supportedSignatureAlgorithms[0] - if (!alg) { - throw new AriesFrameworkError(`No supported algs for key type: ${key.keyType}`) + const allJwt = presentationJson.verifiableCredential?.every((c) => typeof c === 'string') + const allJsonLd = presentationJson.verifiableCredential?.every((c) => typeof c !== 'string') + + // Clients MUST ignore any presentation_submission element included inside a Verifiable Presentation. + const presentationToSign = { ...presentationJson, presentation_submission: undefined } + + let signedPresentation: W3cVerifiablePresentation + if (allJwt) { + signedPresentation = await w3cCredentialService.signPresentation(agentContext, { + format: ClaimFormat.JwtVp, + verificationMethod: verificationMethod.id, + presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation), + alg: this.getSigningAlgorithmForJwtVc(presentationDefinition, verificationMethod), + challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), + domain, + }) + } else if (allJsonLd) { + signedPresentation = await w3cCredentialService.signPresentation(agentContext, { + format: ClaimFormat.LdpVp, + proofType: this.getSigningAlgorithmForLdpVc(presentationDefinition, verificationMethod), + proofPurpose: 'assertionMethod', // TODO: + verificationMethod: verificationMethod.id, + presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation), + challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), + domain, + }) + } else { + throw new AriesFrameworkError( + `Only JWT credentials or JSONLD credentials are supported for a single presentation.` + ) } - const signedPresentation = await w3cCredentialService.signPresentation(agentContext, { - format: ClaimFormat.JwtVp, - verificationMethod: verificationMethod.id, - presentation: w3cPresentation, - alg, - challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), - domain, - }) - return getSphereonW3cVerifiablePresentation(signedPresentation) } } @@ -295,62 +372,4 @@ export class PresentationExchangeService { return verificationMethod } - - /** - * Queries the wallet for credentials that match the given presentation definition. This only does an initial query based on the - * schema of the input descriptors. It does not do any further filtering based on the constraints in the input descriptors. - */ - private async queryCredentialForPresentationDefinition( - agentContext: AgentContext, - presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2 - ) { - const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) - - const query: Array> = [] - - const presentationDefinitionVersion = PEX.definitionVersionDiscovery(presentationDefinition) - - if (!presentationDefinitionVersion.version) { - throw new AriesFrameworkError( - `Unable to determine version for presentation definition. ${ - presentationDefinitionVersion.error ?? 'Unknown error' - }` - ) - } - - if (presentationDefinitionVersion.version === PEVersion.v1) { - const pd = presentationDefinition as PresentationDefinitionV1 - - // The schema.uri can contain either an expanded type, or a context uri - for (const inputDescriptor of pd.input_descriptors) { - for (const schema of inputDescriptor.schema) { - // FIXME: It's currently not possible to query by the `type` of the credential. So we fetch all JWT VCs for now - query.push({ - $or: [{ expandedType: [schema.uri] }, { contexts: [schema.uri] }, { claimFormat: ClaimFormat.JwtVc }], - }) - } - } - } else if (presentationDefinitionVersion.version === PEVersion.v2) { - // FIXME: As PE version 2 does not have the `schema` anymore, we can't query by schema anymore. - // For now we retrieve ALL credentials, as we did the same for V1 with JWT credentials. We probably need - // to find some way to do initial filtering, hopefully if there's a filter on the `type` field or something. - - // FIXME: It's currently not possible to query by the `type` of the credential. So we fetch all JWT VCs for now - query.push({ - $or: [{ claimFormat: ClaimFormat.JwtVc }], - }) - } else { - throw new AriesFrameworkError( - `Unsupported presentation definition version ${presentationDefinitionVersion.version as unknown as string}` - ) - } - - // query the wallet ourselves first to avoid the need to query the pex library for all - // credentials for every proof request - const credentialRecords = await w3cCredentialRepository.findByQuery(agentContext, { - $or: query, - }) - - return credentialRecords - } } diff --git a/packages/openid4vc-holder/src/presentations/fixtures.ts b/packages/openid4vc-holder/src/presentations/fixtures.ts deleted file mode 100644 index 8fa169a956..0000000000 --- a/packages/openid4vc-holder/src/presentations/fixtures.ts +++ /dev/null @@ -1,76 +0,0 @@ -import type { PresentationDefinitionV1 } from '@sphereon/pex-models' - -export const multipleCredentialPresentationDefinition: PresentationDefinitionV1 = { - id: '022c2664-68cc-45cc-b291-789ce8b599eb', - purpose: 'We want to know your name and e-mail address (will not be stored)', - input_descriptors: [ - { - id: 'c2834d0e-3c95-4721-b21a-40e3d7ea2549', - name: 'DBC Conference 2023 Attendee', - purpose: 'To access this portal your DBC Conference 2023 attendance proof is required.', - group: ['A'], - schema: [ - { - uri: 'DBCConferenceAttendee', - required: true, - }, - ], - constraints: { - fields: [ - { - path: ['$.credentialSubject.event.name', '$.vc.credentialSubject.event.name'], - filter: { - type: 'string', - pattern: 'DBC Conference 2023', - }, - }, - ], - }, - }, - { - id: 'c2834d0e-3c95-4721-b21a-40e3d7ea2549', - name: 'Drivers licence', - purpose: - 'Your drivers license is needed to validate your birth date. We do this to prevent fraud with conference tickets.', - group: ['A'], - schema: [ - { - uri: 'NotPresent', - required: true, - }, - ], - }, - ], - submission_requirements: [ - { - rule: 'pick', - count: 2, - from: 'A', - }, - ], -} - -export const dbcPresentationDefinition: PresentationDefinitionV1 = { - id: '022c2664-68cc-45cc-b291-789ce8b599eb', - purpose: 'We want to know your name and e-mail address (will not be stored)', - input_descriptors: [ - { - id: 'c2834d0e-3c95-4721-b21a-40e3d7ea2549', - name: 'DBC Conference 2023 Attendee', - purpose: 'To access this portal your DBC Conference 2023 attendance proof is required.', - group: ['A'], - schema: [ - { - uri: 'DBCConferenceAttendee', - required: true, - }, - ], - }, - ], - submission_requirements: [ - { - rule: 'all', - from: 'A', - }, - ], -} diff --git a/packages/openid4vc-holder/src/presentations/selection/PexCredentialSelection.ts b/packages/openid4vc-holder/src/presentations/selection/PexCredentialSelection.ts index b538b33045..c60d36a608 100644 --- a/packages/openid4vc-holder/src/presentations/selection/PexCredentialSelection.ts +++ b/packages/openid4vc-holder/src/presentations/selection/PexCredentialSelection.ts @@ -1,13 +1,7 @@ import type { PresentationSubmission, PresentationSubmissionRequirement, SubmissionEntry } from './types' import type { W3cCredentialRecord } from '@aries-framework/core' -import type { SelectResults, SubmissionRequirementMatch } from '@sphereon/pex' -import type { - PresentationDefinitionV1, - SubmissionRequirement, - InputDescriptorV1, - PresentationDefinitionV2, - InputDescriptorV2, -} from '@sphereon/pex-models' +import type { IPresentationDefinition, SelectResults, SubmissionRequirementMatch } from '@sphereon/pex' +import type { InputDescriptorV1, InputDescriptorV2, SubmissionRequirement } from '@sphereon/pex-models' import { AriesFrameworkError } from '@aries-framework/core' import { PEX } from '@sphereon/pex' @@ -16,58 +10,68 @@ import { default as jp } from 'jsonpath' import { getSphereonW3cVerifiableCredential } from '../transform' -/** - * Converts a camelCase string to a sentence format (first letter capitalized, rest in lower case). - * i.e. sanitizeString("helloWorld") // returns: 'Hello world' - */ -export function sanitizeString(str: string) { - const result = str.replace(/([a-z0-9])([A-Z])/g, '$1 $2') - let words = result.split(' ') - words = words.map((word, index) => { - if (index === 0) { - return word.charAt(0).toUpperCase() + word.slice(1) - } else { - return word.charAt(0).toLowerCase() + word.slice(1) - } - }) - return words.join(' ') -} +export async function selectCredentialsForRequest( + presentationDefinition: IPresentationDefinition, + credentialRecords: W3cCredentialRecord[], + holderDIDs: string[] +): Promise { + const encodedCredentials = credentialRecords.map((c) => getSphereonW3cVerifiableCredential(c.credential)) + + if (!presentationDefinition) { + throw new AriesFrameworkError('Presentation Definition is required to select credentials for submission.') + } + + if (!encodedCredentials || encodedCredentials.length === 0) { + throw new AriesFrameworkError('No credentials to select from, for the presentation submission.') + } -export function selectCredentialsForRequest( - presentationDefinition: PresentationDefinitionV1, - credentialRecords: W3cCredentialRecord[] -): PresentationSubmission { const pex = new PEX() - const encodedCredentials = credentialRecords.map((c) => getSphereonW3cVerifiableCredential(c.credential)) + // FIXME: there is a function for this in the PEX library, + // but it is not strictly compatible atm + const selectResultsRaw = pex.selectFrom(presentationDefinition, encodedCredentials, { + holderDIDs, + // limitDisclosureSignatureSuites: [], + // restrictToDIDMethods, + // restrictToFormats + }) - const selectResultsRaw = pex.selectFrom(presentationDefinition, encodedCredentials) + if (selectResultsRaw.areRequiredCredentialsPresent === 'error') { + if (selectResultsRaw.errors && selectResultsRaw.errors.length > 0) { + const errorList = selectResultsRaw.errors + .map((e) => `tag '${e.tag}', status '${e.status}', message, '${e.message}'`) + .join('\n') + throw new AriesFrameworkError('Error while selecting credentials for presentation submission.\n' + errorList) + } + + throw new AriesFrameworkError( + 'Error while selecting credentials for presentation submission. Not all required credentials are present.' + ) + } const selectResults = { ...selectResultsRaw, // Map the encoded credential to their respective w3c credential record - verifiableCredential: selectResultsRaw.verifiableCredential?.map((encoded): W3cCredentialRecord => { + verifiableCredential: selectResultsRaw.verifiableCredential?.map((encoded) => { const credentialIndex = encodedCredentials.indexOf(encoded) const credentialRecord = credentialRecords[credentialIndex] - if (!credentialRecord) { - throw new AriesFrameworkError('Unable to find credential in credential records') - } + if (!credentialRecord) throw new AriesFrameworkError('Unable to find credential in credential records.') return credentialRecord }), } const presentationSubmission: PresentationSubmission = { - areRequirementsSatisfied: false, requirements: [], + areRequirementsSatisfied: false, name: presentationDefinition.name, purpose: presentationDefinition.purpose, } // If there's no submission requirements, ALL input descriptors MUST be satisfied if (!presentationDefinition.submission_requirements || presentationDefinition.submission_requirements.length === 0) { - presentationSubmission.requirements = getSubmissionRequirementsAllInputDescriptors( - presentationDefinition, + presentationSubmission.requirements = getSubmissionRequirementsForAllInputDescriptors( + presentationDefinition.input_descriptors, selectResults ) } else { @@ -94,7 +98,7 @@ export function selectCredentialsForRequest( } function getSubmissionRequirements( - presentationDefinition: PresentationDefinitionV1, + presentationDefinition: IPresentationDefinition, selectResults: W3cCredentialRecordSelectResults ): PresentationSubmissionRequirement[] { const submissionRequirements: PresentationSubmissionRequirement[] = [] @@ -102,7 +106,7 @@ function getSubmissionRequirements( // There are submission requirements, so we need to select the input_descriptors // based on the submission requirements for (const submissionRequirement of presentationDefinition.submission_requirements ?? []) { - // Check if the submissionRequirement uses `from_nested`, as we don't support this yet + // Check: if the submissionRequirement uses `from_nested`, as we don't support this yet if (submissionRequirement.from_nested) { throw new AriesFrameworkError( "Presentation definition contains requirement using 'from_nested', which is not supported yet." @@ -114,55 +118,45 @@ function getSubmissionRequirements( throw new AriesFrameworkError("Missing 'from' in submission requirement match") } - // Rule is all if (submissionRequirement.rule === Rules.All) { const selectedSubmission = getSubmissionRequirementRuleAll( submissionRequirement, presentationDefinition, selectResults ) - - // Submission may have requirement that doesn't require a credential to be submitted (e.g. min: 0) - // We use minimization strategy, and thus only disclose the minimum amount of information - // TODO: is this the right place to do this? - if (selectedSubmission.needsCount > 0) { - submissionRequirements.push(selectedSubmission) - } - } - // Rule is Pick - else { + submissionRequirements.push(selectedSubmission) + } else { const selectedSubmission = getSubmissionRequirementRulePick( submissionRequirement, presentationDefinition, selectResults ) - // Submission may have requirement that doesn't require a credential to be submitted (e.g. min: 0) - // We use minimization strategy, and thus only disclose the minimum amount of information - // TODO: is this the right place to do this? - if (selectedSubmission.needsCount > 0) { - submissionRequirements.push(selectedSubmission) - } + submissionRequirements.push(selectedSubmission) } } - return submissionRequirements + // Submission may have requirement that doesn't require a credential to be submitted (e.g. min: 0) + // We use minimization strategy, and thus only disclose the minimum amount of information + const requirementsWithCredentials = submissionRequirements.filter((requirement) => requirement.needsCount > 0) + + return requirementsWithCredentials } -function getSubmissionRequirementsAllInputDescriptors( - presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2, +function getSubmissionRequirementsForAllInputDescriptors( + inputDescriptors: InputDescriptorV1[] | InputDescriptorV2[], selectResults: W3cCredentialRecordSelectResults ): PresentationSubmissionRequirement[] { const submissionRequirements: PresentationSubmissionRequirement[] = [] - for (const inputDescriptor of presentationDefinition.input_descriptors) { + for (const inputDescriptor of inputDescriptors) { const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) submissionRequirements.push({ + rule: 'pick', + needsCount: 1, // Every input descriptor is a distinct requirement, so the count is always 1, + submissionEntry: [submission], isRequirementSatisfied: submission.verifiableCredentials.length >= 1, - submission: [submission], - // Every input descriptor is a separate requirement, so the count is always 1 - needsCount: 1, }) } @@ -171,20 +165,19 @@ function getSubmissionRequirementsAllInputDescriptors( function getSubmissionRequirementRuleAll( submissionRequirement: SubmissionRequirement, - presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2, + presentationDefinition: IPresentationDefinition, selectResults: W3cCredentialRecordSelectResults ) { // Check if there's a 'from'. If not the structure is not as we expect it - if (!submissionRequirement.from) { - throw new AriesFrameworkError("Missing 'from' in submission requirement match") - } + if (!submissionRequirement.from) throw new AriesFrameworkError("Missing 'from' in submission requirement match.") const selectedSubmission: PresentationSubmissionRequirement = { + rule: 'all', + needsCount: 0, name: submissionRequirement.name, purpose: submissionRequirement.purpose, + submissionEntry: [], isRequirementSatisfied: false, - needsCount: 0, - submission: [], } for (const inputDescriptor of presentationDefinition.input_descriptors) { @@ -195,14 +188,14 @@ function getSubmissionRequirementRuleAll( // Rule ALL, so for every input descriptor that matches in this group, we need to add it selectedSubmission.needsCount += 1 - selectedSubmission.submission.push(submission) + selectedSubmission.submissionEntry.push(submission) } return { ...selectedSubmission, // If all submissions have a credential, the requirement is satisfied - isRequirementSatisfied: selectedSubmission.submission.every( + isRequirementSatisfied: selectedSubmission.submissionEntry.every( (submission) => submission.verifiableCredentials.length >= 1 ), } @@ -210,23 +203,21 @@ function getSubmissionRequirementRuleAll( function getSubmissionRequirementRulePick( submissionRequirement: SubmissionRequirement, - presentationDefinition: PresentationDefinitionV1, + presentationDefinition: IPresentationDefinition, selectResults: W3cCredentialRecordSelectResults ) { // Check if there's a 'from'. If not the structure is not as we expect it - if (!submissionRequirement.from) { - throw new AriesFrameworkError("Missing 'from' in submission requirement match") - } + if (!submissionRequirement.from) throw new AriesFrameworkError("Missing 'from' in submission requirement match.") const selectedSubmission: PresentationSubmissionRequirement = { + rule: 'pick', + needsCount: submissionRequirement.count ?? submissionRequirement.min ?? 1, name: submissionRequirement.name, purpose: submissionRequirement.purpose, + // If there's no count, min, or max we assume one credential is required for submission + // however, the exact behavior is not specified in the spec + submissionEntry: [], isRequirementSatisfied: false, - submission: [], - - // TODO: if there's no count, min, max should we then assume the number to include is 1? - // TODO: if there's no count, min, but there is a max. Should we assume the min is 0 or 1? - needsCount: submissionRequirement.count ?? submissionRequirement.min ?? 1, } const satisfiedSubmissions: SubmissionEntry[] = [] @@ -244,22 +235,22 @@ function getSubmissionRequirementRulePick( unsatisfiedSubmissions.push(submission) } - if (satisfiedSubmissions.length === selectedSubmission.needsCount) { - break - } + // If we have found enough credentials to satisfy the requirement, we could stop + // but the user may not want the first x that match, so we continue and return all matches + // if (satisfiedSubmissions.length === selectedSubmission.needsCount) break } return { ...selectedSubmission, - // If there's enough satisfied submissions, the requirement is satisfied - isRequirementSatisfied: satisfiedSubmissions.length === selectedSubmission.needsCount, + // If there are enough satisfied submissions, the requirement is satisfied + isRequirementSatisfied: satisfiedSubmissions.length >= selectedSubmission.needsCount, // if the requirement is satisfied, we only need to return the satisfied submissions // however if the requirement is not satisfied, we include all entries so the wallet could // render which credentials are missing. submission: - satisfiedSubmissions.length === selectedSubmission.needsCount + satisfiedSubmissions.length >= selectedSubmission.needsCount ? satisfiedSubmissions : [...satisfiedSubmissions, ...unsatisfiedSubmissions], } @@ -270,43 +261,32 @@ function getSubmissionForInputDescriptor( selectResults: W3cCredentialRecordSelectResults ): SubmissionEntry { // https://github.com/Sphereon-Opensource/PEX/issues/116 - // FIXME: the match.name is only the id if the input_descriptor has no name - // Find first match - const matches = selectResults.matches?.filter( + // If the input descriptor doesn't contain a name, the name of the match will be the id of the input descriptor that satisfied it + const matchesForInputDescriptor = selectResults.matches?.filter( (m) => m.name === inputDescriptor.id || // FIXME: this is not collision proof as the name doesn't have to be unique m.name === inputDescriptor.name ) - let name = inputDescriptor.name - //TODO: I think this is something that should be done by the user and not the framework. Might miss some details as to why we do this though. - // If there's no name on the input descriptor, but the id does not contain - // any special characters or numbers (so only letters and spaces), - // we will use a sanitized version of the id as the name - if (!name && inputDescriptor.id.match(/^[a-zA-Z ]+$/)) { - name = sanitizeString(inputDescriptor.id) - } - const submissionEntry: SubmissionEntry = { inputDescriptorId: inputDescriptor.id, - name, + name: inputDescriptor.name, purpose: inputDescriptor.purpose, verifiableCredentials: [], } // return early if no matches. - if (!matches?.length) return submissionEntry + if (!matchesForInputDescriptor?.length) return submissionEntry // FIXME: This can return multiple credentials for multiple input_descriptors, // which I think is a bug in the PEX library // Extract all credentials from the match - for (const match of matches) { - submissionEntry.verifiableCredentials = [ - ...submissionEntry.verifiableCredentials, - ...extractCredentialsFromMatch(match, selectResults.verifiableCredential), - ] - } + const verifiableCredentials = matchesForInputDescriptor.flatMap((matchForInputDescriptor) => + extractCredentialsFromMatch(matchForInputDescriptor, selectResults.verifiableCredential) + ) + + submissionEntry.verifiableCredentials = verifiableCredentials return submissionEntry } @@ -315,12 +295,9 @@ function extractCredentialsFromMatch(match: SubmissionRequirementMatch, availabl const verifiableCredentials: W3cCredentialRecord[] = [] for (const vcPath of match.vc_path) { - const [verifiableCredential] = jp.query( - { - verifiableCredential: availableCredentials, - }, - vcPath - ) as [W3cCredentialRecord] + const [verifiableCredential] = jp.query({ verifiableCredential: availableCredentials }, vcPath) as [ + W3cCredentialRecord + ] verifiableCredentials.push(verifiableCredential) } diff --git a/packages/openid4vc-holder/src/presentations/example.md b/packages/openid4vc-holder/src/presentations/selection/example.md similarity index 100% rename from packages/openid4vc-holder/src/presentations/example.md rename to packages/openid4vc-holder/src/presentations/selection/example.md diff --git a/packages/openid4vc-holder/src/presentations/selection/types.ts b/packages/openid4vc-holder/src/presentations/selection/types.ts index b8feee36b5..df0cd2e7f2 100644 --- a/packages/openid4vc-holder/src/presentations/selection/types.ts +++ b/packages/openid4vc-holder/src/presentations/selection/types.ts @@ -41,6 +41,7 @@ export interface PresentationSubmissionRequirement { * Whether the requirement is satisfied. * * If the requirement is not satisfied, the submission will still contain + * entries, but the `verifiableCredentials` list will be empty. */ isRequirementSatisfied: boolean @@ -65,7 +66,7 @@ export interface PresentationSubmissionRequirement { * `isRequirementSatisfied` is `false`, make sure to check the `needsCount` value * to see how many of those submissions needed. */ - submission: SubmissionEntry[] + submissionEntry: SubmissionEntry[] /** * The number of submission entries that are needed to fulfill the requirement. @@ -75,7 +76,12 @@ export interface PresentationSubmissionRequirement { */ needsCount: number - // TODO: add requirement/restriction for input + /** + * The rule that is used to select the credentials for the submission. + * If the rule is `pick`, the user can select which credentials to use for the submission. + * If the rule is `all`, all credentials that satisfy the input descriptor will be used. + */ + rule: 'pick' | 'all' } export interface PresentationSubmission { @@ -110,6 +116,6 @@ export interface PresentationSubmission { /** * Mapping of selected credentials for an input descriptor */ -export interface CredentialsForInputDescriptor { +export interface InputDescriptorToCredentials { [inputDescriptorId: string]: W3cVerifiableCredential[] } diff --git a/packages/openid4vc-holder/src/shared.ts b/packages/openid4vc-holder/src/shared.ts new file mode 100644 index 0000000000..c514f10dbe --- /dev/null +++ b/packages/openid4vc-holder/src/shared.ts @@ -0,0 +1,86 @@ +import type { AgentContext, VerificationMethod, JwaSignatureAlgorithm } from '@aries-framework/core' +import type { DIDDocument, SigningAlgo } from '@sphereon/did-auth-siop' + +import { + AriesFrameworkError, + DidsApi, + TypedArrayEncoder, + getKeyFromVerificationMethod, + getJwkClassFromKeyType, +} from '@aries-framework/core' + +/** + * Returns the JWA Signature Algorithms that are supported by the wallet. + * + * This is an approximation based on the supported key types of the wallet. + * This is not 100% correct as a supporting a key type does not mean you support + * all the algorithms for that key type. However, this needs refactoring of the wallet + * that is planned for the 0.5.0 release. + */ +export function getSupportedJwaSignatureAlgorithms(agentContext: AgentContext): JwaSignatureAlgorithm[] { + const supportedKeyTypes = agentContext.wallet.supportedKeyTypes + + // Extract the supported JWS algs based on the key types the wallet support. + const supportedJwaSignatureAlgorithms = supportedKeyTypes + // Map the supported key types to the supported JWK class + .map(getJwkClassFromKeyType) + // Filter out the undefined values + .filter((jwkClass): jwkClass is Exclude => jwkClass !== undefined) + // Extract the supported JWA signature algorithms from the JWK class + .flatMap((jwkClass) => jwkClass.supportedSignatureAlgorithms) + + return supportedJwaSignatureAlgorithms +} + +export function getSupportedDidMethods(agentContext: AgentContext) { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const supportedDidMethods: Set = new Set() + + for (const resolver of didsApi.config.resolvers) { + resolver.supportedMethods.forEach((method) => supportedDidMethods.add(method)) + } + + return Array.from(supportedDidMethods) +} + +export async function getSuppliedSignatureFromVerificationMethod( + agentContext: AgentContext, + verificationMethod: VerificationMethod +) { + // get the key from the verification method and use the first supported signature algorithm + const key = getKeyFromVerificationMethod(verificationMethod) + const alg = getJwkClassFromKeyType(key.keyType)?.supportedSignatureAlgorithms[0] + if (!alg) throw new AriesFrameworkError(`No supported signature algorithms for key type: ${key.keyType}`) + + const suppliedSignature = { + signature: async (data: string | Uint8Array) => { + if (typeof data !== 'string') throw new AriesFrameworkError("Expected string but received 'Uint8Array'") + const signedData = await agentContext.wallet.sign({ + data: TypedArrayEncoder.fromString(data), + key, + }) + + const signature = TypedArrayEncoder.toBase64URL(signedData) + return signature + }, + alg: alg as unknown as SigningAlgo, + did: verificationMethod.controller, + kid: verificationMethod.id, + } + + return suppliedSignature +} + +export function getResolver(agentContext: AgentContext) { + return { + resolve: async (didUrl: string) => { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const result = await didsApi.resolve(didUrl) + + return { + ...result, + didDocument: result.didDocument?.toJSON() as DIDDocument, + } + }, + } +} diff --git a/packages/openid4vc-holder/tests/fixtures_vp.ts b/packages/openid4vc-holder/tests/fixtures_vp.ts new file mode 100644 index 0000000000..81b5b526da --- /dev/null +++ b/packages/openid4vc-holder/tests/fixtures_vp.ts @@ -0,0 +1,5 @@ +export const waltPortalOpenBadgeJwt = + 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDprZXk6ejZNa29hYkE3TG10amVlQUFHS3FxY3BtaHNkYTZCczJaYXlWUzZMUmF5MmdiWFJKIn0.eyJpc3MiOiJkaWQ6a2V5Ono2TWtvYWJBN0xtdGplZUFBR0txcWNwbWhzZGE2QnMyWmF5VlM2TFJheTJnYlhSSiIsInN1YiI6ImRpZDprZXk6ejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjI3o2TWtwR1I0Z3M0UmMzWnBoNHZqOHdSbmpuQXhnQVBTeGNSOE1BVkt1dFdzcFF6YyIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQuanNvbiJdLCJpZCI6InVybjp1dWlkOjg2ODhkMWQxLTFkN2ItNDYzMC1iNTcwLTcxNGFkNTFjODk0NyIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJPcGVuQmFkZ2VDcmVkZW50aWFsIl0sIm5hbWUiOiJKRkYgeCB2Yy1lZHUgUGx1Z0Zlc3QgMyBJbnRlcm9wZXJhYmlsaXR5IiwiaXNzdWVyIjp7InR5cGUiOlsiUHJvZmlsZSJdLCJpZCI6ImRpZDprZXk6ejZNa29hYkE3TG10amVlQUFHS3FxY3BtaHNkYTZCczJaYXlWUzZMUmF5MmdiWFJKIiwibmFtZSI6IkpvYnMgZm9yIHRoZSBGdXR1cmUgKEpGRikiLCJ1cmwiOiJodHRwczovL3d3dy5qZmYub3JnLyIsImltYWdlIjoiaHR0cHM6Ly93M2MtY2NnLmdpdGh1Yi5pby92Yy1lZC9wbHVnZmVzdC0xLTIwMjIvaW1hZ2VzL0pGRl9Mb2dvTG9ja3VwLnBuZyJ9LCJpc3N1YW5jZURhdGUiOiIyMDIzLTExLTA4VDE1OjE1OjMwLjY3NDI5MzY0MFoiLCJleHBpcmF0aW9uRGF0ZSI6IjIwMjQtMTEtMDdUMTU6MTU6MzAuNjc0MzMyODgwWiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rcEdSNGdzNFJjM1pwaDR2ajh3Um5qbkF4Z0FQU3hjUjhNQVZLdXRXc3BRemMjejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjIiwidHlwZSI6WyJBY2hpZXZlbWVudFN1YmplY3QiXSwiYWNoaWV2ZW1lbnQiOnsiaWQiOiJ1cm46dXVpZDphYzI1NGJkNS04ZmFkLTRiYjEtOWQyOS1lZmQ5Mzg1MzY5MjYiLCJ0eXBlIjpbIkFjaGlldmVtZW50Il0sIm5hbWUiOiJKRkYgeCB2Yy1lZHUgUGx1Z0Zlc3QgMyBJbnRlcm9wZXJhYmlsaXR5IiwiZGVzY3JpcHRpb24iOiJUaGlzIHdhbGxldCBzdXBwb3J0cyB0aGUgdXNlIG9mIFczQyBWZXJpZmlhYmxlIENyZWRlbnRpYWxzIGFuZCBoYXMgZGVtb25zdHJhdGVkIGludGVyb3BlcmFiaWxpdHkgZHVyaW5nIHRoZSBwcmVzZW50YXRpb24gcmVxdWVzdCB3b3JrZmxvdyBkdXJpbmcgSkZGIHggVkMtRURVIFBsdWdGZXN0IDMuIiwiY3JpdGVyaWEiOnsidHlwZSI6IkNyaXRlcmlhIiwibmFycmF0aXZlIjoiV2FsbGV0IHNvbHV0aW9ucyBwcm92aWRlcnMgZWFybmVkIHRoaXMgYmFkZ2UgYnkgZGVtb25zdHJhdGluZyBpbnRlcm9wZXJhYmlsaXR5IGR1cmluZyB0aGUgcHJlc2VudGF0aW9uIHJlcXVlc3Qgd29ya2Zsb3cuIFRoaXMgaW5jbHVkZXMgc3VjY2Vzc2Z1bGx5IHJlY2VpdmluZyBhIHByZXNlbnRhdGlvbiByZXF1ZXN0LCBhbGxvd2luZyB0aGUgaG9sZGVyIHRvIHNlbGVjdCBhdCBsZWFzdCB0d28gdHlwZXMgb2YgdmVyaWZpYWJsZSBjcmVkZW50aWFscyB0byBjcmVhdGUgYSB2ZXJpZmlhYmxlIHByZXNlbnRhdGlvbiwgcmV0dXJuaW5nIHRoZSBwcmVzZW50YXRpb24gdG8gdGhlIHJlcXVlc3RvciwgYW5kIHBhc3NpbmcgdmVyaWZpY2F0aW9uIG9mIHRoZSBwcmVzZW50YXRpb24gYW5kIHRoZSBpbmNsdWRlZCBjcmVkZW50aWFscy4ifSwiaW1hZ2UiOnsiaWQiOiJodHRwczovL3czYy1jY2cuZ2l0aHViLmlvL3ZjLWVkL3BsdWdmZXN0LTMtMjAyMy9pbWFnZXMvSkZGLVZDLUVEVS1QTFVHRkVTVDMtYmFkZ2UtaW1hZ2UucG5nIiwidHlwZSI6IkltYWdlIn19fX0sImp0aSI6InVybjp1dWlkOjg2ODhkMWQxLTFkN2ItNDYzMC1iNTcwLTcxNGFkNTFjODk0NyIsImV4cCI6MTczMDk5MjUzMCwiaWF0IjoxNjk5NDU2NTMwLCJuYmYiOjE2OTk0NTY0NDB9.Qv140_WAK8jzF9ZFRi1zQPRaLpdcTmf3QSVxXjNVrtJLqwW7ePqQoplGpxuvzKGUFhjIMVIRt4EVEcU_j1mfBw' + +export const waltUniversityDegreeJwt = + 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDprZXk6ejZNa29hYkE3TG10amVlQUFHS3FxY3BtaHNkYTZCczJaYXlWUzZMUmF5MmdiWFJKIn0.eyJpc3MiOiJkaWQ6a2V5Ono2TWtvYWJBN0xtdGplZUFBR0txcWNwbWhzZGE2QnMyWmF5VlM2TFJheTJnYlhSSiIsInN1YiI6ImRpZDprZXk6ejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjI3o2TWtwR1I0Z3M0UmMzWnBoNHZqOHdSbmpuQXhnQVBTeGNSOE1BVkt1dFdzcFF6YyIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvZXhhbXBsZXMvdjEiXSwiaWQiOiJ1cm46dXVpZDpmNTkyMzFhMS1jZWJkLTQyNDMtYjQwNy01OWFlOWYxYjRkMzciLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVW5pdmVyc2l0eURlZ3JlZSJdLCJpc3N1ZXIiOnsiaWQiOiJkaWQ6a2V5Ono2TWtvYWJBN0xtdGplZUFBR0txcWNwbWhzZGE2QnMyWmF5VlM2TFJheTJnYlhSSiJ9LCJpc3N1YW5jZURhdGUiOiIyMDIzLTExLTEwVDE0OjUxOjUxLjQ4NTYzNjY5M1oiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6ejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjI3o2TWtwR1I0Z3M0UmMzWnBoNHZqOHdSbmpuQXhnQVBTeGNSOE1BVkt1dFdzcFF6YyIsImRlZ3JlZSI6eyJ0eXBlIjoiQmFjaGVsb3JEZWdyZWUiLCJuYW1lIjoiQmFjaGVsb3Igb2YgU2NpZW5jZSBhbmQgQXJ0cyJ9fX0sImp0aSI6InVybjp1dWlkOmY1OTIzMWExLWNlYmQtNDI0My1iNDA3LTU5YWU5ZjFiNGQzNyIsImlhdCI6MTY5OTYyNzkxMSwibmJmIjoxNjk5NjI3ODIxfQ.IvEhwCLBZ-zEyY1f1AV6T9tBG27f2PoFQi5rzvSNN1Io8x6f4PmtOmyNZsNLAD56pZFgyGKUJomQbQSP5thyBQ' diff --git a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts index 4413bdd339..b9e7071441 100644 --- a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts @@ -1,85 +1,146 @@ import type { KeyDidCreateOptions, VerificationMethod } from '@aries-framework/core' +import type { CreateProofRequestOptions } from '@aries-framework/openid4vc-verifier' +import type { PresentationDefinitionV2 } from '@sphereon/pex-models' import { AskarModule } from '@aries-framework/askar' -import { KeyType, Agent, TypedArrayEncoder, DidKey } from '@aries-framework/core' +import { KeyType, Agent, TypedArrayEncoder, DidKey, W3cJwtVerifiableCredential } from '@aries-framework/core' import { agentDependencies } from '@aries-framework/node' +import { OpenId4VcVerifierModule, staticOpOpenIdConfig } from '@aries-framework/openid4vc-verifier' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' -import { PassBy, ResponseType, Scope, SigningAlgo, SubjectType } from '@sphereon/did-auth-siop' +import { SigningAlgo } from '@sphereon/did-auth-siop' import nock from 'nock' import { OpenId4VcHolderModule } from '../src' -import { getSupportedDidMethods, staticOpSiopConfig } from '../src/presentations/OpenId4VpHolderService' + +import { waltPortalOpenBadgeJwt, waltUniversityDegreeJwt } from './fixtures_vp' + +// id id%22%3A%22test%22%2C%22 +// * = %2A +// TODO: error on sphereon lib PR opened +// TODO: walt issued credentials verification fails due to some time issue || //throw new Error(`Inconsistent issuance dates between JWT claim (${nbfDateAsStr}) and VC value (${issuanceDate})`); +// TODO: error walt no id in presentation definition +// TODO: error walt vc.type is an array not a string thus the filter does not work $.type (should be array according to vc data 1.1) +// TODO: jwt_vc vs jwt_vc_json + +const universityDegreePresentationDefinition: PresentationDefinitionV2 = { + id: 'UniversityDegreeCredential', + input_descriptors: [ + { + id: 'UniversityDegree', + // changed jwt_vc_json to jwt_vc + format: { jwt_vc: { alg: ['EdDSA'] } }, + // changed $.type to $.vc.type + constraints: { + fields: [{ path: ['$.vc.type.*'], filter: { type: 'string', pattern: 'UniversityDegree' } }], + }, + }, + ], +} + +const openBadgePresentationDefinition: PresentationDefinitionV2 = { + id: 'OpenBadgeCredential', + input_descriptors: [ + { + id: 'OpenBadgeCredential', + // changed jwt_vc_json to jwt_vc + format: { jwt_vc: { alg: ['EdDSA'] } }, + // changed $.type to $.vc.type + constraints: { + fields: [{ path: ['$.vc.type.*'], filter: { type: 'string', pattern: 'OpenBadgeCredential' } }], + }, + }, + ], +} + +const combinePresentationDefinitions = ( + presentationDefinitions: PresentationDefinitionV2[] +): PresentationDefinitionV2 => { + return { + id: 'Combined', + input_descriptors: presentationDefinitions.flatMap((p) => p.input_descriptors), + } +} + +const staticOpOpenIdConfigEdDSA = { + ...staticOpOpenIdConfig, + idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA], + requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA], + vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] }, jwt_vp: { alg: [SigningAlgo.EDDSA] } }, +} const modules = { openId4VcHolder: new OpenId4VcHolderModule(), + openId4VcVerifier: new OpenId4VcVerifierModule(), askar: new AskarModule({ ariesAskar }), } describe('OpenId4VcHolder | OpenID4VP', () => { - let rp: Agent - let rpVerificationMethod: VerificationMethod + let verifier: Agent + let verifierVerificationMethod: VerificationMethod - let op: Agent - let opVerificationMethod: VerificationMethod + let holder: Agent + let holderVerificationMethod: VerificationMethod beforeEach(async () => { - rp = new Agent({ + verifier = new Agent({ config: { - label: 'OpenId4VcRp OpenID4VP Test', + label: 'OpenId4VcRp OpenID4VP Test36', walletConfig: { - id: 'openid4vc-rp-openid4vp-test', - key: 'openid4vc-rp-openid4vp-test', + id: 'openid4vc-rp-openid4vp-test37', + key: 'openid4vc-rp-openid4vp-test38', }, }, dependencies: agentDependencies, modules, }) - op = new Agent({ + holder = new Agent({ config: { - label: 'OpenId4VcOp OpenID4VP Test', + label: 'OpenId4VcOp OpenID4VP Test37', walletConfig: { - id: 'openid4vc-op-openid4vp-test', - key: 'openid4vc-op-openid4vp-test', + id: 'openid4vc-op-openid4vp-test38', + key: 'openid4vc-op-openid4vp-test39', }, }, dependencies: agentDependencies, modules, }) - await rp.initialize() - await op.initialize() + await verifier.initialize() + await holder.initialize() - const rpDid = await rp.dids.create({ + const verifierDid = await verifier.dids.create({ method: 'key', - options: { keyType: KeyType.P256 }, - secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598f') }, }) - const rpDidKey = DidKey.fromDid(rpDid.didState.did as string) - const rpKid = `${rpDid.didState.did as string}#${rpDidKey.key.fingerprint}` - const _rpVerificationMethod = rpDid.didState.didDocument?.dereferenceKey(rpKid, ['authentication']) - if (!_rpVerificationMethod) throw new Error('No verification method found') - rpVerificationMethod = _rpVerificationMethod + const verifierDidKey = DidKey.fromDid(verifierDid.didState.did as string) + const verifierKid = `${verifierDid.didState.did as string}#${verifierDidKey.key.fingerprint}` + const _verifierVerificationMethod = verifierDid.didState.didDocument?.dereferenceKey(verifierKid, [ + 'authentication', + ]) + if (!_verifierVerificationMethod) throw new Error('No verification method found') + verifierVerificationMethod = _verifierVerificationMethod - const opDid = await op.dids.create({ + const holderDid = await holder.dids.create({ method: 'key', - options: { keyType: KeyType.P256 }, - secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598f') }, + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, }) - const opDidKey = DidKey.fromDid(opDid.didState.did as string) - const opKid = `${opDid.didState.did as string}#${opDidKey.key.fingerprint}` - const _opVerificationMethod = opDid.didState.didDocument?.dereferenceKey(opKid, ['authentication']) - if (!_opVerificationMethod) throw new Error('No verification method found') - opVerificationMethod = _opVerificationMethod + const holderDidKey = DidKey.fromDid(holderDid.didState.did as string) + const holderKid = `${holderDid.didState.did as string}#${holderDidKey.key.fingerprint}` + const _holderVerificationMethod = holderDid.didState.didDocument?.dereferenceKey(holderKid, ['authentication']) + if (!_holderVerificationMethod) throw new Error('No verification method found') + holderVerificationMethod = _holderVerificationMethod }) afterEach(async () => { - await op.shutdown() - await op.wallet.delete() - await rp.shutdown() - await rp.wallet.delete() + await holder.shutdown() + await holder.wallet.delete() + await verifier.shutdown() + await verifier.wallet.delete() }) describe('Mattr interop', () => { @@ -118,83 +179,293 @@ describe('OpenId4VcHolder | OpenID4VP', () => { // // selectedCredentials: credentials, // //}) // }) + }) + + it('siop request with static metadata', async () => { + const createProofRequestOptions: CreateProofRequestOptions = { + verificationMethod: verifierVerificationMethod, + redirectUri: 'https://acme.com/hello', + holderClientMetadata: staticOpOpenIdConfigEdDSA, + } + + //////////////////////////// RP (create request) //////////////////////////// + const { proofRequest, proofRequestMetadata } = await verifier.modules.openId4VcVerifier.createProofRequest( + createProofRequestOptions + ) - xit('test against sphereon itself', async () => { - const clientMetadata = { - subject_syntax_types_supported: getSupportedDidMethods(rp.context), - responseTypesSupported: [ResponseType.ID_TOKEN], - scopesSupported: [Scope.OPENID], - subjectTypesSupported: [SubjectType.PAIRWISE], - idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256], - requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA, SigningAlgo.ES256], - passBy: PassBy.VALUE, + //////////////////////////// OP (validate and parse the request) //////////////////////////// + const result = await holder.modules.openId4VcHolder.resolveProofRequest(proofRequest) + + //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// + + if (result.proofType == 'presentation') throw new Error('Expected an authenticationRequest') + + //////////////////////////// OP (accept the verified request) //////////////////////////// + const { submittedResponse } = await holder.modules.openId4VcHolder.acceptAuthenticationRequest( + result.authenticationRequest, + holderVerificationMethod + ) + + //////////////////////////// RP (verify the response) //////////////////////////// + + const verifiedAuthResponseWithJWT = await verifier.modules.openId4VcVerifier.verifyProofResponse( + submittedResponse, + { + createProofRequestOptions, + proofRequestMetadata, } + ) - //////////////////////////// RP (create request) //////////////////////////// - const { authorizationRequestUri, relyingParty } = await rp.modules.openId4VcHolder.createRequest({ - verificationMethod: rpVerificationMethod, - redirect_url: 'https://acme.com/hello', - // TODO: if provided this way client metadata is not resolved vor the verification method - clientMetadata: clientMetadata, - }) - - //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await op.modules.openId4VcHolder.resolveRequest(authorizationRequestUri) - - //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// - // TODO: User interaction - - //////////////////////////// OP (accept the verified request) //////////////////////////// - const authRespWithJWT = await op.modules.openId4VcHolder.acceptRequest(result, opVerificationMethod) - - //////////////////////////// RP (verify the response) //////////////////////////// - const verifiedAuthResponseWithJWT = await relyingParty.verifyAuthorizationResponse( - authRespWithJWT.response.payload, - { - audience: 'https://acme.com/hello', - } - ) - - expect(verifiedAuthResponseWithJWT.idToken).toBeDefined() - expect(verifiedAuthResponseWithJWT.idToken?.payload.state).toMatch('b32f0087fc9816eb813fd11f') - expect(verifiedAuthResponseWithJWT.idToken?.payload.nonce).toMatch('qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg') - }) + const { state, challenge } = proofRequestMetadata + expect(verifiedAuthResponseWithJWT.idTokenPayload).toBeDefined() + expect(verifiedAuthResponseWithJWT.idTokenPayload.state).toMatch(state) + expect(verifiedAuthResponseWithJWT.idTokenPayload.nonce).toMatch(challenge) }) - it('jajaja', async () => { - nock('https://helloworld').get('/.well-known/openid-configuration').reply(200, staticOpSiopConfig) + const getConfig = () => { + return staticOpOpenIdConfigEdDSA + } + + // TODO: not working yet + xit('siop request with issuer', async () => { + nock('https://helloworld.com') + .get('/.well-known/openid-configuration') + .reply(200, getConfig()) + .get('/.well-known/openid-configuration') + .reply(200, getConfig()) + .get('/.well-known/openid-configuration') + .reply(200, getConfig()) + .get('/.well-known/openid-configuration') + .reply(200, getConfig()) + + const createProofRequestOptions: CreateProofRequestOptions = { + verificationMethod: verifierVerificationMethod, + redirectUri: 'https://acme.com/hello', + // TODO: if provided this way client metadata is not resolved for the verification method + issuer: 'https://helloworld.com', + } //////////////////////////// RP (create request) //////////////////////////// - const { authorizationRequestUri, relyingParty } = await rp.modules.openId4VcHolder.createRequest({ - verificationMethod: rpVerificationMethod, - redirect_url: 'https://acme.com/hello', - // TODO: if provided this way client metadata is not resolved vor the verification method - // TODO: rename to verifierMetadata? - clientMetadata: { - passBy: PassBy.REFERENCE, - reference_uri: 'https://helloworld/.well-known/openid-configuration', - }, - }) + const { proofRequest, proofRequestMetadata } = await verifier.modules.openId4VcVerifier.createProofRequest( + createProofRequestOptions + ) //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await op.modules.openId4VcHolder.resolveRequest(authorizationRequestUri) + const result = await holder.modules.openId4VcHolder.resolveProofRequest(proofRequest) //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// - // TODO: User interaction + + if (result.proofType == 'presentation') throw new Error('Expected a proofType') //////////////////////////// OP (accept the verified request) //////////////////////////// - const authRespWithJWT = await op.modules.openId4VcHolder.acceptRequest(result, opVerificationMethod) + const { submittedResponse } = await holder.modules.openId4VcHolder.acceptAuthenticationRequest( + result.authenticationRequest, + holderVerificationMethod + ) //////////////////////////// RP (verify the response) //////////////////////////// - const verifiedAuthResponseWithJWT = await relyingParty.verifyAuthorizationResponse( - authRespWithJWT.response.payload, + + const verifiedProofPresponse = await verifier.modules.openId4VcVerifier.verifyProofResponse(submittedResponse, { + createProofRequestOptions, + proofRequestMetadata, + }) + + const { state, challenge } = proofRequestMetadata + expect(verifiedProofPresponse.idTokenPayload).toBeDefined() + expect(verifiedProofPresponse.idTokenPayload.state).toMatch(state) + expect(verifiedProofPresponse.idTokenPayload.nonce).toMatch(challenge) + }) + + it('resolving vp request with no credentials errors', async () => { + const createProofRequestOptions: CreateProofRequestOptions = { + verificationMethod: verifierVerificationMethod, + redirectUri: 'https://acme.com/hello', + holderClientMetadata: staticOpOpenIdConfigEdDSA, + presentationDefinition: openBadgePresentationDefinition, + } + + const { proofRequest } = await verifier.modules.openId4VcVerifier.createProofRequest(createProofRequestOptions) + + //////////////////////////// OP (validate and parse the request) //////////////////////////// + await expect(holder.modules.openId4VcHolder.resolveProofRequest(proofRequest)).rejects.toThrow() + }) + + it('resolving vp request with wrong credentials errors', async () => { + await holder.w3cCredentials.storeCredential({ + credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), + }) + + const createProofRequestOptions: CreateProofRequestOptions = { + verificationMethod: verifierVerificationMethod, + redirectUri: 'https://acme.com/hello', + holderClientMetadata: staticOpOpenIdConfigEdDSA, + presentationDefinition: openBadgePresentationDefinition, + } + + const { proofRequest } = await verifier.modules.openId4VcVerifier.createProofRequest(createProofRequestOptions) + + //////////////////////////// OP (validate and parse the request) //////////////////////////// + await expect(holder.modules.openId4VcHolder.resolveProofRequest(proofRequest)).rejects.toThrow() + }) + + it('resolving vp request with multiple credentials in wallet only allows selecting the correct ones', async () => { + await holder.w3cCredentials.storeCredential({ + credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), + }) + + await holder.w3cCredentials.storeCredential({ + credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), + }) + + const createProofRequestOptions: CreateProofRequestOptions = { + verificationMethod: verifierVerificationMethod, + redirectUri: 'https://acme.com/hello', + holderClientMetadata: staticOpOpenIdConfigEdDSA, + presentationDefinition: openBadgePresentationDefinition, + } + + const { proofRequest } = await verifier.modules.openId4VcVerifier.createProofRequest(createProofRequestOptions) + + //////////////////////////// OP (validate and parse the request) //////////////////////////// + + const result = await holder.modules.openId4VcHolder.resolveProofRequest(proofRequest) + if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') + + const { presentationRequest, selectResults } = result + expect(selectResults.areRequirementsSatisfied).toBeTruthy() + expect(selectResults.requirements.length).toBe(1) + expect(selectResults.requirements[0].needsCount).toBe(1) + expect(selectResults.requirements[0].submissionEntry.length).toBe(1) + expect(selectResults.requirements[0].submissionEntry[0].inputDescriptorId).toBe('OpenBadgeCredential') + + expect(presentationRequest.presentationDefinitions[0].definition).toMatchObject(openBadgePresentationDefinition) + }) + + it('resolving vp request with multiple credentials in wallet select the correct credentials from the wallet', async () => { + await holder.w3cCredentials.storeCredential({ + credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), + }) + + await holder.w3cCredentials.storeCredential({ + credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), + }) + + const createProofRequestOptions: CreateProofRequestOptions = { + verificationMethod: verifierVerificationMethod, + redirectUri: 'https://acme.com/hello', + holderClientMetadata: staticOpOpenIdConfigEdDSA, + presentationDefinition: combinePresentationDefinitions([ + openBadgePresentationDefinition, + universityDegreePresentationDefinition, + ]), + } + + const { proofRequest } = await verifier.modules.openId4VcVerifier.createProofRequest(createProofRequestOptions) + + //////////////////////////// OP (validate and parse the request) //////////////////////////// + + const result = await holder.modules.openId4VcHolder.resolveProofRequest(proofRequest) + if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') + + const { selectResults } = result + expect(selectResults.areRequirementsSatisfied).toBeTruthy() + expect(selectResults.requirements.length).toBe(2) + expect(selectResults.requirements[0].needsCount).toBe(1) + expect(selectResults.requirements[0].submissionEntry.length).toBe(1) + expect(selectResults.requirements[1].needsCount).toBe(1) + expect(selectResults.requirements[1].submissionEntry.length).toBe(1) + + expect(selectResults.requirements[0].submissionEntry[0].inputDescriptorId).toBe('OpenBadgeCredential') + + expect(selectResults.requirements[1].submissionEntry[0].inputDescriptorId).toBe('UniversityDegree') + }) + + it('expect vp request with single requested credential to succeed', async () => { + await holder.w3cCredentials.storeCredential({ + credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), + }) + + const createProofRequestOptions: CreateProofRequestOptions = { + verificationMethod: verifierVerificationMethod, + redirectUri: 'https://acme.com/hello', + holderClientMetadata: staticOpOpenIdConfigEdDSA, + presentationDefinition: openBadgePresentationDefinition, + } + + const { proofRequest, proofRequestMetadata } = await verifier.modules.openId4VcVerifier.createProofRequest( + createProofRequestOptions + ) + + //////////////////////////// OP (validate and parse the request) //////////////////////////// + const result = await holder.modules.openId4VcHolder.resolveProofRequest(proofRequest) + if (result.proofType === 'authentication') throw new Error('Expected a proofRequest') + + //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// + // Select the appropriate credentials + + result.selectResults.requirements[0] + + if (!result.selectResults.areRequirementsSatisfied) { + throw new Error('Requirements are not satisfied.') + } + + //////////////////////////// OP (accept the verified request) //////////////////////////// + const { submittedResponse, status } = await holder.modules.openId4VcHolder.acceptPresentationRequest( + result.presentationRequest, { - audience: 'https://acme.com/hello', + submission: result.selectResults, + submissionEntryIndexes: [0], } ) - expect(verifiedAuthResponseWithJWT.idToken).toBeDefined() - expect(verifiedAuthResponseWithJWT.idToken?.payload.state).toMatch('b32f0087fc9816eb813fd11f') - expect(verifiedAuthResponseWithJWT.idToken?.payload.nonce).toMatch('qBrR7mqnY3Qr49dAZycPF8FzgE83m6H0c2l0bzP4xSg') + expect(status).toBe(404) + + // The RP MUST validate that the aud (audience) Claim contains the value of the client_id + // that the RP sent in the Authorization Request as an audience. + // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. + const { idTokenPayload, submission } = await verifier.modules.openId4VcVerifier.verifyProofResponse( + submittedResponse, + { + createProofRequestOptions, + proofRequestMetadata, + } + ) + + const { state, challenge } = proofRequestMetadata + expect(idTokenPayload).toBeDefined() + expect(idTokenPayload.state).toMatch(state) + expect(idTokenPayload.nonce).toMatch(challenge) + + expect(submission).toBeDefined() }) + + // it('edited walt vp request', async () => { + // const credential = W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt) + // await holder.w3cCredentials.storeCredential({ credential }) + + // const authorizationRequestUri = + // 'openid4vp://authorize?response_type=vp_token&client_id=https%3A%2F%2Fverifier.portal.walt.id%2Fopenid4vc%2Fverify&response_mode=direct_post&state=97509d5c-2dd2-490b-8617-577f45e3b6d0&presentation_definition=%7B%22id%22%3A%22test%22%2C%22input_descriptors%22%3A%5B%7B%22id%22%3A%22OpenBadgeCredential%22%2C%22format%22%3A%7B%22jwt_vc%22%3A%7B%22alg%22%3A%5B%22EdDSA%22%5D%7D%7D%2C%22constraints%22%3A%7B%22fields%22%3A%5B%7B%22path%22%3A%5B%22%24.vc.type.%2A%22%5D%2C%22filter%22%3A%7B%22type%22%3A%22string%22%2C%22pattern%22%3A%22OpenBadgeCredential%22%7D%7D%5D%7D%7D%5D%7D&client_id_scheme=redirect_uri&response_uri=https%3A%2F%2Fverifier.portal.walt.id%2Fopenid4vc%2Fverify%2F97509d5c-2dd2-490b-8617-577f45e3b6d0' + + // //////////////////////////// OP (validate and parse the request) //////////////////////////// + // const result = await holder.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) + // if (result.proofType === 'authentication') throw new Error('Expected a proofRequest') + + // //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// + // // Select the appropriate credentials + + // const { presentationRequest, selectResults } = result + // result.selectResults.requirements[0] + + // if (!result.selectResults.areRequirementsSatisfied) { + // throw new Error('Requirements are not satisfied.') + // } + + // //////////////////////////// OP (accept the verified request) //////////////////////////// + // const responseStatus = await holder.modules.openId4VcHolder.acceptPresentationRequest(presentationRequest, { + // submission: selectResults, + // submissionEntryIndexes: [0], + // }) + + // expect(responseStatus.ok).toBeTruthy() + // }) }) diff --git a/packages/openid4vc-verifier/package.json b/packages/openid4vc-verifier/package.json index e59c2d5b0e..590c9ef732 100644 --- a/packages/openid4vc-verifier/package.json +++ b/packages/openid4vc-verifier/package.json @@ -25,7 +25,7 @@ }, "dependencies": { "@aries-framework/core": "0.4.2", - "@sphereon/did-auth-siop": "^0.4.2", + "@sphereon/did-auth-siop": "^0.5.0-unstable.7", "@sphereon/ssi-types": "^0.17.5" }, "devDependencies": { diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts index b80f1f0c75..3ed3fa577d 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts @@ -1,4 +1,8 @@ -import type { IssueCredentialOptions, SendCredentialOfferOptions } from './OpenId4VcVerifierServiceOptions' +import type { + CreateProofRequestOptions, + ProofPayload, + VerifyProofResponseOptions, +} from './OpenId4VcVerifierServiceOptions' import { injectable, AgentContext } from '@aries-framework/core' @@ -17,11 +21,34 @@ export class OpenId4VcVerifierApi { this.openId4VcVerifierService = openId4VcVerifierService } - public sendCredentialOffer(options: SendCredentialOfferOptions) { - // TODO: Implement + /** + * Creates a proof request with the provided options. + * The proof request can be a SIOP request (authentication) or VP request querying verifiable credentials). + * Metadata about the holder can be provided by either passing the @see HolderClientMetadata or by passing the issuer URL. + * If the issuer URL is provided, the metadata will be retrieved from the issuer hosted OpenID configuration. + * If neither the holder metadata nor the issuer URL is provided, a static configuration defined in @link https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-static-configuration-values + * If a presentation definition is provided, a VP request will be created, querying the holder verifiable credentials according to the specifics of the presentation definition. + * @param options.verificationMethod - The method used for verification. + * @param options.redirect_url - The URL to redirect to after verification. + * @param options.holderClientMetadata - Optional metadata about the holder client. + * @param options.issuer - Optional issuer of the proof request. + * @param options.presentationDefinition - Optional presentation definition for the proof request. + * @returns @see ProofRequestWithMetadata object containing the proof request and metadata for verifying the proof response. + */ + public async createProofRequest(options: CreateProofRequestOptions) { + return await this.openId4VcVerifierService.createProofRequest(this.agentContext, options) } - public issueCredential(options: IssueCredentialOptions) { - // TODO: Implement + /** + * Verifies a proof response with the provided options. + * The proof response validates the idToken, the signature of the received Verifiable Presentation, + * as well as that the structure of the Verifiable Presentation matches the provided presentation definition. + * + * @param options.createProofRequestOptions - The options used to create the proof request. + * @param options.proofRequestMetadata - Metadata about the proof request. + * @returns @see VerifiedProofResponse object containing the idTokenPayload and the verified submission. + */ + public async verifyProofResponse(proofPayload: ProofPayload, options: VerifyProofResponseOptions) { + return await this.openId4VcVerifierService.verifyProofResponse(this.agentContext, proofPayload, options) } } diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts index 4c5f2891d5..9935cee914 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts @@ -1,12 +1,46 @@ +import type { + VerifyProofResponseOptions, + ProofRequestWithMetadata, + CreateProofRequestOptions, + ProofRequestMetadata, + VerifiedProofResponse, +} from './OpenId4VcVerifierServiceOptions' +import type { AgentContext, W3cVerifyPresentationResult } from '@aries-framework/core' +import type { + AuthorizationResponsePayload, + ClientMetadataOpts, + PresentationDefinitionWithLocation, + PresentationVerificationCallback, + SigningAlgo, +} from '@sphereon/did-auth-siop' + import { InjectionSymbols, - JwsService, Logger, - W3cCredentialRepository, W3cCredentialService, inject, injectable, + AriesFrameworkError, + W3cJsonLdVerifiablePresentation, + JsonTransformer, } from '@aries-framework/core' +import { + RP, + ResponseIss, + RevocationVerification, + SupportedVersion, + ResponseMode, + PropertyTarget, + ResponseType, + CheckLinkedDomain, + PresentationDefinitionLocation, + PassBy, + VerificationMode, +} from '@sphereon/did-auth-siop' + +import { staticOpOpenIdConfig, staticOpSiopConfig } from './OpenId4VcVerifierServiceOptions' +import { getSupportedDidMethods, getSuppliedSignatureFromVerificationMethod, getResolver } from './shared' +import { getSupportedJwaSignatureAlgorithms } from './utils/signatureAlgorithms' /** * @internal @@ -15,18 +49,230 @@ import { export class OpenId4VcVerifierService { private logger: Logger private w3cCredentialService: W3cCredentialService - private w3cCredentialRepository: W3cCredentialRepository - private jwsService: JwsService - - public constructor( - @inject(InjectionSymbols.Logger) logger: Logger, - w3cCredentialService: W3cCredentialService, - w3cCredentialRepository: W3cCredentialRepository, - jwsService: JwsService - ) { + + public constructor(@inject(InjectionSymbols.Logger) logger: Logger, w3cCredentialService: W3cCredentialService) { this.w3cCredentialService = w3cCredentialService - this.w3cCredentialRepository = w3cCredentialRepository - this.jwsService = jwsService this.logger = logger } + + public async getRelyingParty( + agentContext: AgentContext, + createProofRequestOptions: CreateProofRequestOptions, + proofRequestMetadata?: ProofRequestMetadata + ) { + const { + issuer, + redirectUri, + presentationDefinition, + verificationMethod, + holderClientMetadata: _holderClientMetadata, + } = createProofRequestOptions + + const isVpRequest = presentationDefinition !== undefined + + let holderClientMetadata: ClientMetadataOpts + if (_holderClientMetadata) { + // use the provided client metadata + holderClientMetadata = _holderClientMetadata + } else if (issuer) { + // Use OpenId Discovery to get the client metadata + let reference_uri = issuer + if (!issuer.endsWith('/.well-known/openid-configuration')) { + reference_uri = issuer + '/.well-known/openid-configuration' + } + holderClientMetadata = { reference_uri, passBy: PassBy.REFERENCE, targets: PropertyTarget.REQUEST_OBJECT } + } else if (isVpRequest) { + // if neither clientMetadata nor issuer is provided, use a static config + holderClientMetadata = staticOpOpenIdConfig + } else { + // if neither clientMetadata nor issuer is provided, use a static config + holderClientMetadata = staticOpSiopConfig + } + + const { signature, did, kid, alg } = await getSuppliedSignatureFromVerificationMethod( + agentContext, + verificationMethod + ) + + // Check if the OpenId Provider (Holder) can validate the request signature provided by the Relying Party (Verifier) + const requestObjectSigningAlgValuesSupported = holderClientMetadata.requestObjectSigningAlgValuesSupported + if (requestObjectSigningAlgValuesSupported && !requestObjectSigningAlgValuesSupported.includes(alg)) { + throw new AriesFrameworkError( + [ + `Cannot sign authorization request with '${alg}' that isn't supported by the OpenId Provider.`, + `Supported algorithms are ${requestObjectSigningAlgValuesSupported}`, + ].join('\n') + ) + } + + // Check if the Relying Party (Verifier) can validate the IdToken provided by the OpenId Provider (Holder) + const idTokenSigningAlgValuesSupported = holderClientMetadata.idTokenSigningAlgValuesSupported + const rpSupportedSignatureAlgorithms = getSupportedJwaSignatureAlgorithms(agentContext) as unknown as SigningAlgo[] + + if (idTokenSigningAlgValuesSupported) { + const possibleIdTokenSigningAlgValues = Array.isArray(idTokenSigningAlgValuesSupported) + ? idTokenSigningAlgValuesSupported.filter((value) => rpSupportedSignatureAlgorithms.includes(value)) + : [idTokenSigningAlgValuesSupported].filter((value) => rpSupportedSignatureAlgorithms.includes(value)) + + if (!possibleIdTokenSigningAlgValues) { + throw new AriesFrameworkError( + [ + `The OpenId Provider supports no signature algorithms that are supported by the Relying Party.`, + `Relying Party supported algorithms are ${rpSupportedSignatureAlgorithms}.`, + `OpenId Provider supported algorithms are ${idTokenSigningAlgValuesSupported}.`, + ].join('\n') + ) + } + } + + const authorizationEndpoint = holderClientMetadata.authorization_endpoint ?? isVpRequest ? 'openid:' : 'siopv2:' + + // Check: audience must be set to the issuer with dynamic disc otherwise self-issed.me/v2. + const builder = RP.builder() + .withClientId(verificationMethod.id) + .withRedirectUri(redirectUri) + .withRequestByValue() + .withIssuer(ResponseIss.SELF_ISSUED_V2) + .withSuppliedSignature(signature, did, kid, alg) + .withSupportedVersions([SupportedVersion.SIOPv2_D11, SupportedVersion.SIOPv2_D12_OID4VP_D18]) + .withClientMetadata(holderClientMetadata) + .withCustomResolver(getResolver(agentContext)) + .withResponseMode(ResponseMode.POST) + .withResponseType(isVpRequest ? [ResponseType.ID_TOKEN, ResponseType.VP_TOKEN] : ResponseType.ID_TOKEN) + .withRequestBy(PassBy.VALUE) + .withAuthorizationEndpoint(authorizationEndpoint) + .withCheckLinkedDomain(CheckLinkedDomain.NEVER) // check + .withRevocationVerification(RevocationVerification.NEVER) + // .withWellknownDIDVerifyCallback + // .withEventEmitter + // .withSessionManager // For now we use no session manager + + if (proofRequestMetadata) { + builder.withPresentationVerification( + this.handlePresentationResponse(agentContext, { challenge: proofRequestMetadata.challenge }) + ) + } + + if (presentationDefinition) { + builder.withPresentationDefinition({ definition: presentationDefinition }, [ + PropertyTarget.REQUEST_OBJECT, + PropertyTarget.AUTHORIZATION_REQUEST, + ]) + } + + const supportedDidMethods = getSupportedDidMethods(agentContext) + for (const supportedDidMethod of supportedDidMethods) { + builder.addDidMethod(supportedDidMethod) + } + + return builder.build() + } + + public async createProofRequest( + agentContext: AgentContext, + options: CreateProofRequestOptions + ): Promise { + const [noncePart1, noncePart2, state, correlationId] = await generateRandomValues(agentContext, 4) + const challenge = noncePart1 + noncePart2 + + const relyingParty = await this.getRelyingParty(agentContext, { ...options }) + + const authorizationRequest = await relyingParty.createAuthorizationRequest({ + correlationId, + nonce: challenge, + state, + }) + + const authorizationRequestUri = await authorizationRequest.uri() + const encodedAuthorizationRequestUri = authorizationRequestUri.encodedUri + + return { + proofRequest: encodedAuthorizationRequestUri, + proofRequestMetadata: { + correlationId, + challenge, + state, + }, + } + } + + public async verifyProofResponse( + agentContext: AgentContext, + authorizationResponsePayload: AuthorizationResponsePayload, + options: VerifyProofResponseOptions + ): Promise { + const { createProofRequestOptions, proofRequestMetadata } = options + const { state, challenge, correlationId } = proofRequestMetadata + + const relyingParty = await this.getRelyingParty(agentContext, createProofRequestOptions, proofRequestMetadata) + + const presentationDefinition = createProofRequestOptions.presentationDefinition + + let presentationDefinitionsWithLocation: [PresentationDefinitionWithLocation] | undefined + if (presentationDefinition) { + presentationDefinitionsWithLocation = [ + { + definition: presentationDefinition, + location: PresentationDefinitionLocation.CLAIMS_VP_TOKEN, // For now we always use the VP_TOKEN + }, + ] + } + + const response = await relyingParty.verifyAuthorizationResponse(authorizationResponsePayload, { + audience: createProofRequestOptions.verificationMethod.id, + correlationId, + nonce: challenge, + state, + presentationDefinitions: presentationDefinitionsWithLocation, + verification: { + mode: VerificationMode.INTERNAL, + resolveOpts: { noUniversalResolverFallback: true, resolver: getResolver(agentContext) }, + }, + }) + + const idTokenPayload = await response.authorizationResponse.idToken.payload() + + return { + idTokenPayload: idTokenPayload, // TODO: return something else + submission: response.oid4vpSubmission, + } + } + + private handlePresentationResponse( + agentContext: AgentContext, + options: { challenge: string } + ): PresentationVerificationCallback { + const { challenge } = options + return async (encodedPresentation, presentationSubmission) => { + this.logger.debug(`Presentation response '${encodedPresentation}'`) + this.logger.debug(`Presentation submission `, presentationSubmission) + + if (!encodedPresentation) { + throw new AriesFrameworkError('Did not receive a presentation for verification') + } + + let verificationResult: W3cVerifyPresentationResult + if (typeof encodedPresentation === 'string') { + // the presentation is in jwt format (automatically converted to W3cJwtVerifiablePresentation) + const presentation = encodedPresentation + verificationResult = await this.w3cCredentialService.verifyPresentation(agentContext, { + presentation: presentation, + challenge, + }) + } else { + const presentation = JsonTransformer.fromJSON(encodedPresentation, W3cJsonLdVerifiablePresentation) + verificationResult = await this.w3cCredentialService.verifyPresentation(agentContext, { + presentation: presentation, + challenge, + }) + } + + return { verified: verificationResult.isValid } + } + } +} + +async function generateRandomValues(agentContext: AgentContext, count: number) { + const randomValuesPromises = Array.from({ length: count }, () => agentContext.wallet.generateNonce()) + return await Promise.all(randomValuesPromises) } diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts index 0de5ea82f2..d59f85b431 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts @@ -1,7 +1,68 @@ -export interface IssueCredentialOptions { - tobedefined: true +import type { VerificationMethod } from '@aries-framework/core' +import type { + IDTokenPayload, + VerifiedOpenID4VPSubmission, + ClientMetadataOpts, + AuthorizationResponsePayload, +} from '@sphereon/did-auth-siop' +import type { IPresentationDefinition } from '@sphereon/pex' + +import { ResponseType, PassBy, Scope, SigningAlgo, SubjectType } from '@sphereon/did-auth-siop' + +export type HolderClientMetadata = ClientMetadataOpts & { authorization_endpoint?: string } + +export interface CreateProofRequestOptions { + verificationMethod: VerificationMethod + redirectUri: string + holderClientMetadata?: HolderClientMetadata + issuer?: string + presentationDefinition?: IPresentationDefinition +} + +export type ProofRequest = string + +export interface ProofRequestMetadata { + correlationId: string + challenge: string + state: string +} + +export type ProofRequestWithMetadata = { + proofRequest: ProofRequest + proofRequestMetadata: ProofRequestMetadata +} + +export interface VerifyProofResponseOptions { + createProofRequestOptions: CreateProofRequestOptions + proofRequestMetadata: ProofRequestMetadata +} + +export interface VerifiedProofResponse { + idTokenPayload: IDTokenPayload + submission: VerifiedOpenID4VPSubmission | undefined +} + +export type ProofPayload = AuthorizationResponsePayload + +export const staticOpSiopConfig: HolderClientMetadata = { + authorization_endpoint: 'siopv2:', + subject_syntax_types_supported: ['urn:ietf:params:oauth:jwk-thumbprint'], + responseTypesSupported: [ResponseType.ID_TOKEN], + scopesSupported: [Scope.OPENID], + subjectTypesSupported: [SubjectType.PAIRWISE], + idTokenSigningAlgValuesSupported: [SigningAlgo.ES256], + requestObjectSigningAlgValuesSupported: [SigningAlgo.ES256], + passBy: PassBy.VALUE, } -export interface SendCredentialOfferOptions { - tobedefined: true +export const staticOpOpenIdConfig: HolderClientMetadata = { + authorization_endpoint: 'openid:', + subject_syntax_types_supported: ['urn:ietf:params:oauth:jwk-thumbprint'], + responseTypesSupported: [ResponseType.ID_TOKEN, ResponseType.VP_TOKEN], + scopesSupported: [Scope.OPENID], + subjectTypesSupported: [SubjectType.PAIRWISE], + idTokenSigningAlgValuesSupported: [SigningAlgo.ES256], + requestObjectSigningAlgValuesSupported: [SigningAlgo.ES256], + passBy: PassBy.VALUE, + vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.ES256] }, jwt_vp: { alg: [SigningAlgo.ES256] } }, } diff --git a/packages/openid4vc-verifier/src/index.ts b/packages/openid4vc-verifier/src/index.ts index f606b366c0..86f27cd6fd 100644 --- a/packages/openid4vc-verifier/src/index.ts +++ b/packages/openid4vc-verifier/src/index.ts @@ -3,4 +3,10 @@ export * from './OpenId4VcVerifierModule' export * from './OpenId4VcVerifierService' // Contains internal types, so we don't export everything -export {} from './OpenId4VcVerifierServiceOptions' +export { + HolderClientMetadata, + staticOpOpenIdConfig, + staticOpSiopConfig, + CreateProofRequestOptions, + ProofRequestWithMetadata, +} from './OpenId4VcVerifierServiceOptions' diff --git a/packages/openid4vc-verifier/src/shared.ts b/packages/openid4vc-verifier/src/shared.ts new file mode 100644 index 0000000000..c514f10dbe --- /dev/null +++ b/packages/openid4vc-verifier/src/shared.ts @@ -0,0 +1,86 @@ +import type { AgentContext, VerificationMethod, JwaSignatureAlgorithm } from '@aries-framework/core' +import type { DIDDocument, SigningAlgo } from '@sphereon/did-auth-siop' + +import { + AriesFrameworkError, + DidsApi, + TypedArrayEncoder, + getKeyFromVerificationMethod, + getJwkClassFromKeyType, +} from '@aries-framework/core' + +/** + * Returns the JWA Signature Algorithms that are supported by the wallet. + * + * This is an approximation based on the supported key types of the wallet. + * This is not 100% correct as a supporting a key type does not mean you support + * all the algorithms for that key type. However, this needs refactoring of the wallet + * that is planned for the 0.5.0 release. + */ +export function getSupportedJwaSignatureAlgorithms(agentContext: AgentContext): JwaSignatureAlgorithm[] { + const supportedKeyTypes = agentContext.wallet.supportedKeyTypes + + // Extract the supported JWS algs based on the key types the wallet support. + const supportedJwaSignatureAlgorithms = supportedKeyTypes + // Map the supported key types to the supported JWK class + .map(getJwkClassFromKeyType) + // Filter out the undefined values + .filter((jwkClass): jwkClass is Exclude => jwkClass !== undefined) + // Extract the supported JWA signature algorithms from the JWK class + .flatMap((jwkClass) => jwkClass.supportedSignatureAlgorithms) + + return supportedJwaSignatureAlgorithms +} + +export function getSupportedDidMethods(agentContext: AgentContext) { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const supportedDidMethods: Set = new Set() + + for (const resolver of didsApi.config.resolvers) { + resolver.supportedMethods.forEach((method) => supportedDidMethods.add(method)) + } + + return Array.from(supportedDidMethods) +} + +export async function getSuppliedSignatureFromVerificationMethod( + agentContext: AgentContext, + verificationMethod: VerificationMethod +) { + // get the key from the verification method and use the first supported signature algorithm + const key = getKeyFromVerificationMethod(verificationMethod) + const alg = getJwkClassFromKeyType(key.keyType)?.supportedSignatureAlgorithms[0] + if (!alg) throw new AriesFrameworkError(`No supported signature algorithms for key type: ${key.keyType}`) + + const suppliedSignature = { + signature: async (data: string | Uint8Array) => { + if (typeof data !== 'string') throw new AriesFrameworkError("Expected string but received 'Uint8Array'") + const signedData = await agentContext.wallet.sign({ + data: TypedArrayEncoder.fromString(data), + key, + }) + + const signature = TypedArrayEncoder.toBase64URL(signedData) + return signature + }, + alg: alg as unknown as SigningAlgo, + did: verificationMethod.controller, + kid: verificationMethod.id, + } + + return suppliedSignature +} + +export function getResolver(agentContext: AgentContext) { + return { + resolve: async (didUrl: string) => { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const result = await didsApi.resolve(didUrl) + + return { + ...result, + didDocument: result.didDocument?.toJSON() as DIDDocument, + } + }, + } +} diff --git a/packages/openid4vc-verifier/src/utils/signatureAlgorithms.ts b/packages/openid4vc-verifier/src/utils/signatureAlgorithms.ts new file mode 100644 index 0000000000..4c8859396d --- /dev/null +++ b/packages/openid4vc-verifier/src/utils/signatureAlgorithms.ts @@ -0,0 +1,26 @@ +import type { AgentContext, JwaSignatureAlgorithm } from '@aries-framework/core' + +import { getJwkClassFromKeyType } from '@aries-framework/core' + +/** + * Returns the JWA Signature Algorithms that are supported by the wallet. + * + * This is an approximation based on the supported key types of the wallet. + * This is not 100% correct as a supporting a key type does not mean you support + * all the algorithms for that key type. However, this needs refactoring of the wallet + * that is planned for the 0.5.0 release. + */ +export function getSupportedJwaSignatureAlgorithms(agentContext: AgentContext): JwaSignatureAlgorithm[] { + const supportedKeyTypes = agentContext.wallet.supportedKeyTypes + + // Extract the supported JWS algs based on the key types the wallet support. + const supportedJwaSignatureAlgorithms = supportedKeyTypes + // Map the supported key types to the supported JWK class + .map(getJwkClassFromKeyType) + // Filter out the undefined values + .filter((jwkClass): jwkClass is Exclude => jwkClass !== undefined) + // Extract the supported JWA signature algorithms from the JWK class + .flatMap((jwkClass) => jwkClass.supportedSignatureAlgorithms) + + return supportedJwaSignatureAlgorithms +} diff --git a/yarn.lock b/yarn.lock index 9148667c56..575ad49909 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2408,28 +2408,6 @@ resolved "https://registry.yarnpkg.com/@sovpro/delimited-stream/-/delimited-stream-1.1.0.tgz#4334bba7ee241036e580fdd99c019377630d26b4" integrity sha512-kQpk267uxB19X3X2T1mvNMjyvIEonpNSHrMlK5ZaBU6aZxw7wPbpgKJOjHN3+/GPVpXgAV9soVT2oyHpLkLtyw== -"@sphereon/did-auth-siop@^0.4.2": - version "0.4.2" - resolved "https://registry.yarnpkg.com/@sphereon/did-auth-siop/-/did-auth-siop-0.4.2.tgz#fd5b606dc1e85fe95506580fb691f384c22c2df5" - integrity sha512-uzeX530K6WxqA17X4s8jEeUb9xFymXhE+UM0uGzg7by41ohsAQ0HOoOswDxflAUap+9STXcLwgjAkQPBDPD8+w== - dependencies: - "@astronautlabs/jsonpath" "^1.1.2" - "@sphereon/did-uni-client" "^0.6.0" - "@sphereon/pex" "^2.1.2" - "@sphereon/pex-models" "^2.1.0" - "@sphereon/ssi-types" "^0.17.4" - "@sphereon/wellknown-dids-client" "^0.1.3" - cross-fetch "^3.1.8" - did-jwt "6.11.6" - did-resolver "^4.1.0" - events "^3.3.0" - language-tags "^1.0.8" - multiformats "^11.0.2" - querystring "^0.2.1" - sha.js "^2.4.11" - uint8arrays "^3.1.1" - uuid "^9.0.0" - "@sphereon/did-auth-siop@^0.5.0-unstable.7": version "0.5.0-unstable.7" resolved "https://registry.yarnpkg.com/@sphereon/did-auth-siop/-/did-auth-siop-0.5.0-unstable.7.tgz#3867ffe44f9289ce85f1f41241d98464e0acb4c4" @@ -2497,15 +2475,15 @@ "@sphereon/ssi-types" "0.17.2" uuid "^9.0.0" -"@sphereon/pex-models@^2.0.3", "@sphereon/pex-models@^2.1.0", "@sphereon/pex-models@^2.1.1": +"@sphereon/pex-models@^2.1.1": version "2.1.1" resolved "https://registry.yarnpkg.com/@sphereon/pex-models/-/pex-models-2.1.1.tgz#399e529db2a7e3b9abbd7314cdba619ceb6cb758" integrity sha512-0UX/CMwgiJSxzuBn6SLOTSKkm+uPq3dkNjl8w4EtppXp6zBB4lQMd1mJX7OifX5Bp5vPUfoz7bj2B+yyDtbZww== -"@sphereon/pex@2.2.1-unstable.0", "@sphereon/pex@^2.2.1-unstable.0": - version "2.2.1-unstable.0" - resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-2.2.1-unstable.0.tgz#58c339f578d487db5e7ac54a79871c58bdbe63ff" - integrity sha512-gAO4+pUSdN+kDHaB1D7oCSn1AZcKOeH3IXv7NdPkf1U4J/sJpLEXA46gsG8SBTX4e45CRGwt8c5F7vPmIwX7XQ== +"@sphereon/pex@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-2.2.0.tgz#df72bcea2ae5a744fc236d5cef050d28f68f8a77" + integrity sha512-NSEyrzWorPb9K5XuznoUMsI3N4S+0xHxPtUaRP/Zkf1hfOJ7Tj+D5XsOslq171DMoTfVVvYQgha4IL2l3ukZdw== dependencies: "@astronautlabs/jsonpath" "^1.1.2" "@sphereon/pex-models" "^2.1.1" @@ -2516,14 +2494,14 @@ nanoid "^3.3.6" string.prototype.matchall "^4.0.8" -"@sphereon/pex@^2.1.2": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-2.1.2.tgz#99ecaf9dcf62bdbaf3a24db28abc0e165051a894" - integrity sha512-x2lo4iRWfKj2NQIGVZIMhwYrCllRY7j0U9t3g0pkx3mxSUwXhQwEYAcBU+AlS5rGv1kLUXRhHDGPUwt7Y0kHgw== +"@sphereon/pex@2.2.1-unstable.0": + version "2.2.1-unstable.0" + resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-2.2.1-unstable.0.tgz#58c339f578d487db5e7ac54a79871c58bdbe63ff" + integrity sha512-gAO4+pUSdN+kDHaB1D7oCSn1AZcKOeH3IXv7NdPkf1U4J/sJpLEXA46gsG8SBTX4e45CRGwt8c5F7vPmIwX7XQ== dependencies: "@astronautlabs/jsonpath" "^1.1.2" - "@sphereon/pex-models" "^2.0.3" - "@sphereon/ssi-types" "^0.15.1" + "@sphereon/pex-models" "^2.1.1" + "@sphereon/ssi-types" "^0.17.5" ajv "^8.12.0" ajv-formats "^2.1.1" jwt-decode "^3.1.2" @@ -2537,14 +2515,7 @@ dependencies: jwt-decode "^3.1.2" -"@sphereon/ssi-types@^0.15.1": - version "0.15.1" - resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.15.1.tgz#120926e1b633b616026ebe3dd6e73ed6fe350110" - integrity sha512-NFpgcVHIU8YQ2OkCHpw9YVa5bIDBcfSbp0kvwC0iZa0du1tr3148fV2Xm4ilcLeRNvUKL5BbDEdHl1WuQkmoyw== - dependencies: - jwt-decode "^3.1.2" - -"@sphereon/ssi-types@^0.17.4", "@sphereon/ssi-types@^0.17.5": +"@sphereon/ssi-types@^0.17.5": version "0.17.5" resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.17.5.tgz#7b4de0326e7c2993ab816caeef6deaea41a5f65f" integrity sha512-hoQOkeOtshvIzNAG+HTqcKxeGssLVfwX7oILHJgs6VMb1GhR6QlqjMAxflDxZ/8Aq2R0I6fEPWmf73zAXY2X2Q== @@ -10504,11 +10475,6 @@ query-string@^7.0.1: split-on-first "^1.0.0" strict-uri-encode "^2.0.0" -querystring@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.1.tgz#40d77615bb09d16902a85c3e38aa8b5ed761c2dd" - integrity sha512-wkvS7mL/JMugcup3/rMitHmd9ecIGd2lhFhK9N3UUQ450h66d1r3Y9nvXzQAW1Lq+wyx61k/1pfKS5KuKiyEbg== - queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" From 73193612b9c9f5ceaaf7ce19bb01c87114670ef3 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Sun, 12 Nov 2023 15:16:23 +0100 Subject: [PATCH 032/115] fix: do not throw if wrong or no crendentials are in wallet Signed-off-by: Martin Auer --- .../selection/PexCredentialSelection.ts | 23 ++++--------------- .../tests/openid4vp-holder.e2e.test.ts | 14 ++++++++--- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/packages/openid4vc-holder/src/presentations/selection/PexCredentialSelection.ts b/packages/openid4vc-holder/src/presentations/selection/PexCredentialSelection.ts index c60d36a608..c6ffb7d5f8 100644 --- a/packages/openid4vc-holder/src/presentations/selection/PexCredentialSelection.ts +++ b/packages/openid4vc-holder/src/presentations/selection/PexCredentialSelection.ts @@ -21,14 +21,9 @@ export async function selectCredentialsForRequest( throw new AriesFrameworkError('Presentation Definition is required to select credentials for submission.') } - if (!encodedCredentials || encodedCredentials.length === 0) { - throw new AriesFrameworkError('No credentials to select from, for the presentation submission.') - } - const pex = new PEX() - // FIXME: there is a function for this in the PEX library, - // but it is not strictly compatible atm + // FIXME: there is a function for this in the VP library, but it is not usable atm const selectResultsRaw = pex.selectFrom(presentationDefinition, encodedCredentials, { holderDIDs, // limitDisclosureSignatureSuites: [], @@ -36,19 +31,6 @@ export async function selectCredentialsForRequest( // restrictToFormats }) - if (selectResultsRaw.areRequiredCredentialsPresent === 'error') { - if (selectResultsRaw.errors && selectResultsRaw.errors.length > 0) { - const errorList = selectResultsRaw.errors - .map((e) => `tag '${e.tag}', status '${e.status}', message, '${e.message}'`) - .join('\n') - throw new AriesFrameworkError('Error while selecting credentials for presentation submission.\n' + errorList) - } - - throw new AriesFrameworkError( - 'Error while selecting credentials for presentation submission. Not all required credentials are present.' - ) - } - const selectResults = { ...selectResultsRaw, // Map the encoded credential to their respective w3c credential record @@ -86,6 +68,9 @@ export async function selectCredentialsForRequest( 'Presentation Definition does not require any credentials. Optional credentials are not included in the presentation submission.' ) } + if (selectResultsRaw.areRequiredCredentialsPresent === 'error') { + return presentationSubmission + } return { ...presentationSubmission, diff --git a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts index b9e7071441..a03958271c 100644 --- a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts @@ -276,7 +276,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { expect(verifiedProofPresponse.idTokenPayload.nonce).toMatch(challenge) }) - it('resolving vp request with no credentials errors', async () => { + it('resolving vp request with no credentials', async () => { const createProofRequestOptions: CreateProofRequestOptions = { verificationMethod: verifierVerificationMethod, redirectUri: 'https://acme.com/hello', @@ -287,7 +287,11 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const { proofRequest } = await verifier.modules.openId4VcVerifier.createProofRequest(createProofRequestOptions) //////////////////////////// OP (validate and parse the request) //////////////////////////// - await expect(holder.modules.openId4VcHolder.resolveProofRequest(proofRequest)).rejects.toThrow() + const result = await holder.modules.openId4VcHolder.resolveProofRequest(proofRequest) + if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') + + expect(result.selectResults.areRequirementsSatisfied).toBeFalsy() + expect(result.selectResults.requirements.length).toBe(1) }) it('resolving vp request with wrong credentials errors', async () => { @@ -304,8 +308,12 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const { proofRequest } = await verifier.modules.openId4VcVerifier.createProofRequest(createProofRequestOptions) + const result = await holder.modules.openId4VcHolder.resolveProofRequest(proofRequest) + if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') + //////////////////////////// OP (validate and parse the request) //////////////////////////// - await expect(holder.modules.openId4VcHolder.resolveProofRequest(proofRequest)).rejects.toThrow() + expect(result.selectResults.areRequirementsSatisfied).toBeFalsy() + expect(result.selectResults.requirements.length).toBe(1) }) it('resolving vp request with multiple credentials in wallet only allows selecting the correct ones', async () => { From 83325e1fe1795c5ec2cf52a61482879e047fca75 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Sun, 12 Nov 2023 15:20:15 +0100 Subject: [PATCH 033/115] fix: don't return empty presentation submission Signed-off-by: Martin Auer --- packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts index 9935cee914..47fb7a5575 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts @@ -234,7 +234,7 @@ export class OpenId4VcVerifierService { return { idTokenPayload: idTokenPayload, // TODO: return something else - submission: response.oid4vpSubmission, + submission: presentationDefinition ? response.oid4vpSubmission : undefined, } } From c21b06380886c69060548eb435a7254884ca1c56 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Sun, 12 Nov 2023 15:20:52 +0100 Subject: [PATCH 034/115] test: remove old test Signed-off-by: Martin Auer --- .../tests/openid4vp-holder.e2e.test.ts | 47 ++----------------- 1 file changed, 5 insertions(+), 42 deletions(-) diff --git a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts index a03958271c..a5d6b107f4 100644 --- a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts @@ -143,44 +143,6 @@ describe('OpenId4VcHolder | OpenID4VP', () => { await verifier.wallet.delete() }) - describe('Mattr interop', () => { - // Not working yet. Once it works, we can mock the requests/responses - // xit('Should succesfuly share a proof with MATTR launchpad', async () => { - // // Store needed credential / did / key - // await agent.w3cCredentials.storeCredential({ - // credential: W3cJwtVerifiableCredential.fromSerializedJwt( - // 'eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bGF1bmNocGFkLnZpaS5lbGVjdHJvbi5tYXR0cmxhYnMuaW8jNkJoRk1DR1RKZyJ9.eyJpc3MiOiJkaWQ6d2ViOmxhdW5jaHBhZC52aWkuZWxlY3Ryb24ubWF0dHJsYWJzLmlvIiwic3ViIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJuYmYiOjE2OTYwMjI5NDksImV4cCI6MTcyNzY0NTM0OSwidmMiOnsibmFtZSI6IkV4YW1wbGUgVW5pdmVyc2l0eSBEZWdyZWUiLCJkZXNjcmlwdGlvbiI6IkpGRiBQbHVnZmVzdCAzIE9wZW5CYWRnZSBDcmVkZW50aWFsIiwiY3JlZGVudGlhbEJyYW5kaW5nIjp7ImJhY2tncm91bmRDb2xvciI6IiM0NjRjNDkifSwiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL21hdHRyLmdsb2JhbC9jb250ZXh0cy92Yy1leHRlbnNpb25zL3YyIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjIuanNvbiIsImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9leHRlbnNpb25zLmpzb24iLCJodHRwczovL3czaWQub3JnL3ZjLXJldm9jYXRpb24tbGlzdC0yMDIwL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJPcGVuQmFkZ2VDcmVkZW50aWFsIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJ0eXBlIjpbIkFjaGlldmVtZW50U3ViamVjdCJdLCJhY2hpZXZlbWVudCI6eyJpZCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vYWNoaWV2ZW1lbnRzLzIxc3QtY2VudHVyeS1za2lsbHMvdGVhbXdvcmsiLCJuYW1lIjoiVGVhbXdvcmsiLCJ0eXBlIjpbIkFjaGlldmVtZW50Il0sImltYWdlIjp7ImlkIjoiaHR0cHM6Ly93M2MtY2NnLmdpdGh1Yi5pby92Yy1lZC9wbHVnZmVzdC0zLTIwMjMvaW1hZ2VzL0pGRi1WQy1FRFUtUExVR0ZFU1QzLWJhZGdlLWltYWdlLnBuZyIsInR5cGUiOiJJbWFnZSJ9LCJjcml0ZXJpYSI6eyJuYXJyYXRpdmUiOiJUZWFtIG1lbWJlcnMgYXJlIG5vbWluYXRlZCBmb3IgdGhpcyBiYWRnZSBieSB0aGVpciBwZWVycyBhbmQgcmVjb2duaXplZCB1cG9uIHJldmlldyBieSBFeGFtcGxlIENvcnAgbWFuYWdlbWVudC4ifSwiZGVzY3JpcHRpb24iOiJUaGlzIGJhZGdlIHJlY29nbml6ZXMgdGhlIGRldmVsb3BtZW50IG9mIHRoZSBjYXBhY2l0eSB0byBjb2xsYWJvcmF0ZSB3aXRoaW4gYSBncm91cCBlbnZpcm9ubWVudC4ifX0sImlzc3VlciI6eyJpZCI6ImRpZDp3ZWI6bGF1bmNocGFkLnZpaS5lbGVjdHJvbi5tYXR0cmxhYnMuaW8iLCJuYW1lIjoiRXhhbXBsZSBVbml2ZXJzaXR5IiwiaWNvblVybCI6Imh0dHBzOi8vdzNjLWNjZy5naXRodWIuaW8vdmMtZWQvcGx1Z2Zlc3QtMS0yMDIyL2ltYWdlcy9KRkZfTG9nb0xvY2t1cC5wbmciLCJpbWFnZSI6Imh0dHBzOi8vdzNjLWNjZy5naXRodWIuaW8vdmMtZWQvcGx1Z2Zlc3QtMS0yMDIyL2ltYWdlcy9KRkZfTG9nb0xvY2t1cC5wbmcifX19.HUYvivfEH2-yBXUq6t5gEZu1NY7_6tjsWojQvYbpRL_md5TyAmwn-LyfcPLyrQpgJcu08XjFp8smXFMfYJEqCQ' - // ), - // }) - // see https://github.com/hyperledger/aries-framework-javascript/pull/1604#discussion_r1376347318 - // const key = await op.wallet.createKey({ - // keyType: KeyType.Ed25519, - // privateKey: TypedArrayEncoder.fromString('00000000000000000000000000000000'), - // }) - // const did = new DidKey(key) - // await agent.dids.import({ - // did: 'did:key:z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', - // }) - // const openId4VpHolderService = agent.dependencyManager.resolve(OpenId4VpHolderService) - // const { selectResults, verifiedAuthorizationRequest } = - // await openId4VpHolderService.selectCredentialForProofRequest(agent.context, { - // authorizationRequest: - // 'openid4vp://authorize?client_id=https%3A%2F%2Flaunchpad.mattrlabs.com%2Fapi%2Fvp%2Fcallback&client_id_scheme=redirect_uri&response_uri=https%3A%2F%2Flaunchpad.mattrlabs.com%2Fapi%2Fvp%2Fcallback&response_type=vp_token&response_mode=direct_post&presentation_definition_uri=https%3A%2F%2Flaunchpad.mattrlabs.com%2Fapi%2Fvp%2Frequest%3Fstate%3D9b2nQuoLQkW0bX_vk24qjg&nonce=u-Wg1dR5wo5IqIr8ilshMQ&state=9b2nQuoLQkW0bX_vk24qjg', - // }) - // if (!selectResults.areRequirementsSatisfied) { - // throw new Error('Requirements are not satisfied.') - // } - // const credentialRecords = selectResults.requirements - // .flatMap((requirement) => requirement.submission.flatMap((submission) => submission.verifiableCredentials)) - // .filter((credentialRecord): credentialRecord is W3cCredentialRecord => credentialRecord !== undefined) - // const credentials = credentialRecords.map((credentialRecord) => credentialRecord.credential) - // //await openId4VpHolderService.shareProof(agent.context, { - // // verifiedAuthorizationRequest, - // // selectedCredentials: credentials, - // //}) - // }) - }) - it('siop request with static metadata', async () => { const createProofRequestOptions: CreateProofRequestOptions = { verificationMethod: verifierVerificationMethod, @@ -208,7 +170,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { //////////////////////////// RP (verify the response) //////////////////////////// - const verifiedAuthResponseWithJWT = await verifier.modules.openId4VcVerifier.verifyProofResponse( + const { idTokenPayload, submission } = await verifier.modules.openId4VcVerifier.verifyProofResponse( submittedResponse, { createProofRequestOptions, @@ -217,9 +179,10 @@ describe('OpenId4VcHolder | OpenID4VP', () => { ) const { state, challenge } = proofRequestMetadata - expect(verifiedAuthResponseWithJWT.idTokenPayload).toBeDefined() - expect(verifiedAuthResponseWithJWT.idTokenPayload.state).toMatch(state) - expect(verifiedAuthResponseWithJWT.idTokenPayload.nonce).toMatch(challenge) + expect(submission).toBe(undefined) + expect(idTokenPayload).toBeDefined() + expect(idTokenPayload.state).toMatch(state) + expect(idTokenPayload.nonce).toMatch(challenge) }) const getConfig = () => { From 0ec0832fae3833dccf13b3666e4a2f2ee1e90ee7 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Sun, 12 Nov 2023 15:21:10 +0100 Subject: [PATCH 035/115] refactor: rename variables and function Signed-off-by: Martin Auer --- .../PresentationExchangeService.ts | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts b/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts index f7fc2e1575..183a2eb90b 100644 --- a/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts +++ b/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts @@ -113,7 +113,7 @@ export class PresentationExchangeService { return credentialRecords } - private addCredentialForSubjectWithInputDescriptorId( + private addCredentialToSubjectInputDescriptor( subjectsToInputDescriptors: ProofStructure, subjectId: string, inputDescriptorId: string, @@ -148,14 +148,14 @@ export class PresentationExchangeService { throw new AriesFrameworkError('Missing required credential subject for creating the presentation.') } - this.addCredentialForSubjectWithInputDescriptorId(proofStructure, subjectId, inputDescriptorId, credential) + this.addCredentialToSubjectInputDescriptor(proofStructure, subjectId, inputDescriptorId, credential) }) }) const verifiablePresentationResults: VerifiablePresentationResult[] = [] const subjectToInputDescriptors = Object.entries(proofStructure) - for (const [subjectId, inputDescriptorsToCredentials] of subjectToInputDescriptors) { + for (const [subjectId, subjectInputDescriptorsToCredentials] of subjectToInputDescriptors) { // Determine a suitable verification method for the presentation const verificationMethod = await this.getVerificationMethodForSubjectId(agentContext, subjectId) @@ -166,29 +166,29 @@ export class PresentationExchangeService { // We create a presentation for each subject // Thus for each subject we need to filter all the related input descriptors and credentials // FIXME: cast to V1, as tsc errors for strange reasons if not - const inputDescriptorsForVp = (presentationDefinition as PresentationDefinitionV1).input_descriptors.filter( - (inputDescriptor) => inputDescriptor.id in inputDescriptorsToCredentials + const inputDescriptorsForSubject = (presentationDefinition as PresentationDefinitionV1).input_descriptors.filter( + (inputDescriptor) => inputDescriptor.id in subjectInputDescriptorsToCredentials ) // Get all the credentials associated with the input descriptors - const credentialsForVp = Object.values(inputDescriptorsToCredentials) - .flatMap((inputDescriptors) => inputDescriptors) + const credentialsForSubject = Object.values(subjectInputDescriptorsToCredentials) + .flatMap((credentials) => credentials) .map(getSphereonW3cVerifiableCredential) - const presentationDefinitionForVp: IPresentationDefinition = { + const presentationDefinitionForSubject: IPresentationDefinition = { ...presentationDefinition, - input_descriptors: inputDescriptorsForVp, + input_descriptors: inputDescriptorsForSubject, // We remove the submission requirements, as it will otherwise fail to create the VP - // FIXME: Will this cause issue for creating the credential? Need to run tests + // TODO: Will this cause issue for creating the credential? Need to run tests submission_requirements: undefined, } - // FIXME: Q1: is holder always subject id, what if there are multiple subjects??? - // FIXME: Q2: What about proofType, proofPurpose verification method for multiple subjects? + // TODO: Q1: is holder always subject id, what if there are multiple subjects??? + // TODO: Q2: What about proofType, proofPurpose verification method for multiple subjects? const verifiablePresentationResult = await this.pex.verifiablePresentationFrom( - presentationDefinitionForVp, - credentialsForVp, + presentationDefinitionForSubject, + credentialsForSubject, this.getPresentationSignCallback(agentContext, verificationMethod), { holderDID: subjectId, @@ -334,7 +334,7 @@ export class PresentationExchangeService { signedPresentation = await w3cCredentialService.signPresentation(agentContext, { format: ClaimFormat.LdpVp, proofType: this.getSigningAlgorithmForLdpVc(presentationDefinition, verificationMethod), - proofPurpose: 'assertionMethod', // TODO: + proofPurpose: 'assertionMethod', // TODO: authentication verificationMethod: verificationMethod.id, presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation), challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), From 1d1a75227d4c311395cced85696109545e2758c5 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Sun, 12 Nov 2023 15:27:16 +0100 Subject: [PATCH 036/115] test: expect submitting a wrong submission to fail Signed-off-by: Martin Auer --- .../tests/openid4vp-holder.e2e.test.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts index a5d6b107f4..a9d70bb312 100644 --- a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts @@ -279,6 +279,45 @@ describe('OpenId4VcHolder | OpenID4VP', () => { expect(result.selectResults.requirements.length).toBe(1) }) + it('expect submitting a wrong submission to fail', async () => { + await holder.w3cCredentials.storeCredential({ + credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), + }) + + await holder.w3cCredentials.storeCredential({ + credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), + }) + + const createProofRequestOptions: CreateProofRequestOptions = { + verificationMethod: verifierVerificationMethod, + redirectUri: 'https://acme.com/hello', + holderClientMetadata: staticOpOpenIdConfigEdDSA, + presentationDefinition: openBadgePresentationDefinition, + } + + const { proofRequest: openBadge } = await verifier.modules.openId4VcVerifier.createProofRequest( + createProofRequestOptions + ) + const { proofRequest: university } = await verifier.modules.openId4VcVerifier.createProofRequest({ + ...createProofRequestOptions, + presentationDefinition: universityDegreePresentationDefinition, + }) + + //////////////////////////// OP (validate and parse the request) //////////////////////////// + + const resolvedOpenBadge = await holder.modules.openId4VcHolder.resolveProofRequest(openBadge) + const resolvedUniversityDegree = await holder.modules.openId4VcHolder.resolveProofRequest(university) + if (resolvedOpenBadge.proofType !== 'presentation') throw new Error('expected prooftype presentation') + if (resolvedUniversityDegree.proofType !== 'presentation') throw new Error('expected prooftype presentation') + + await expect( + holder.modules.openId4VcHolder.acceptPresentationRequest(resolvedOpenBadge.presentationRequest, { + submission: resolvedUniversityDegree.selectResults, + submissionEntryIndexes: [0], + }) + ).rejects.toThrow() + }) + it('resolving vp request with multiple credentials in wallet only allows selecting the correct ones', async () => { await holder.w3cCredentials.storeCredential({ credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), From 3158f839570c8db81fc0308b4f7f177fc56eeb9a Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Sun, 12 Nov 2023 16:37:09 +0100 Subject: [PATCH 037/115] tests: add more test cases Signed-off-by: Martin Auer --- .../tests/openid4vp-holder.e2e.test.ts | 84 ++++++++++++++++--- 1 file changed, 73 insertions(+), 11 deletions(-) diff --git a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts index a9d70bb312..db1b3f4eb8 100644 --- a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts @@ -168,6 +168,9 @@ describe('OpenId4VcHolder | OpenID4VP', () => { holderVerificationMethod ) + expect(result.authenticationRequest.authorizationRequestPayload.redirect_uri).toBe('https://acme.com/hello') + expect(result.authenticationRequest.issuer).toBe(verifierVerificationMethod.controller) + //////////////////////////// RP (verify the response) //////////////////////////// const { idTokenPayload, submission } = await verifier.modules.openId4VcVerifier.verifyProofResponse( @@ -185,21 +188,17 @@ describe('OpenId4VcHolder | OpenID4VP', () => { expect(idTokenPayload.nonce).toMatch(challenge) }) - const getConfig = () => { - return staticOpOpenIdConfigEdDSA - } - // TODO: not working yet xit('siop request with issuer', async () => { nock('https://helloworld.com') .get('/.well-known/openid-configuration') - .reply(200, getConfig()) + .reply(200, staticOpOpenIdConfigEdDSA) .get('/.well-known/openid-configuration') - .reply(200, getConfig()) + .reply(200, staticOpOpenIdConfigEdDSA) .get('/.well-known/openid-configuration') - .reply(200, getConfig()) + .reply(200, staticOpOpenIdConfigEdDSA) .get('/.well-known/openid-configuration') - .reply(200, getConfig()) + .reply(200, staticOpOpenIdConfigEdDSA) const createProofRequestOptions: CreateProofRequestOptions = { verificationMethod: verifierVerificationMethod, @@ -370,7 +369,9 @@ describe('OpenId4VcHolder | OpenID4VP', () => { ]), } - const { proofRequest } = await verifier.modules.openId4VcVerifier.createProofRequest(createProofRequestOptions) + const { proofRequest, proofRequestMetadata } = await verifier.modules.openId4VcVerifier.createProofRequest( + createProofRequestOptions + ) //////////////////////////// OP (validate and parse the request) //////////////////////////// @@ -384,10 +385,67 @@ describe('OpenId4VcHolder | OpenID4VP', () => { expect(selectResults.requirements[0].submissionEntry.length).toBe(1) expect(selectResults.requirements[1].needsCount).toBe(1) expect(selectResults.requirements[1].submissionEntry.length).toBe(1) - expect(selectResults.requirements[0].submissionEntry[0].inputDescriptorId).toBe('OpenBadgeCredential') - expect(selectResults.requirements[1].submissionEntry[0].inputDescriptorId).toBe('UniversityDegree') + + const { submittedResponse } = await holder.modules.openId4VcHolder.acceptPresentationRequest( + result.presentationRequest, + { + submission: result.selectResults, + submissionEntryIndexes: [0, 0], + } + ) + + const { idTokenPayload, submission } = await verifier.modules.openId4VcVerifier.verifyProofResponse( + submittedResponse, + { + createProofRequestOptions, + proofRequestMetadata, + } + ) + + expect(idTokenPayload).toBeDefined() + expect(submission).toBeDefined() + expect(submission?.presentationDefinitions).toHaveLength(1) + expect(submission?.submissionData.definition_id).toBe('Combined') + expect(submission?.presentations).toHaveLength(1) + expect(submission?.presentations[0].vcs).toHaveLength(2) + expect(submission?.presentations[0].vcs[0].credential.type).toContain('OpenBadgeCredential') + expect(submission?.presentations[0].vcs[1].credential.type).toContain('UniversityDegree') + }) + + it('expect accepting a proof request with only a partial set of requirements to error', async () => { + await holder.w3cCredentials.storeCredential({ + credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), + }) + + await holder.w3cCredentials.storeCredential({ + credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), + }) + + const createProofRequestOptions: CreateProofRequestOptions = { + verificationMethod: verifierVerificationMethod, + redirectUri: 'https://acme.com/hello', + holderClientMetadata: staticOpOpenIdConfigEdDSA, + presentationDefinition: combinePresentationDefinitions([ + openBadgePresentationDefinition, + universityDegreePresentationDefinition, + ]), + } + + const { proofRequest } = await verifier.modules.openId4VcVerifier.createProofRequest(createProofRequestOptions) + + //////////////////////////// OP (validate and parse the request) //////////////////////////// + + const result = await holder.modules.openId4VcHolder.resolveProofRequest(proofRequest) + if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') + + await expect( + holder.modules.openId4VcHolder.acceptPresentationRequest(result.presentationRequest, { + submission: result.selectResults, + submissionEntryIndexes: [0], + }) + ).rejects.toThrow() }) it('expect vp request with single requested credential to succeed', async () => { @@ -447,6 +505,10 @@ describe('OpenId4VcHolder | OpenID4VP', () => { expect(idTokenPayload.nonce).toMatch(challenge) expect(submission).toBeDefined() + expect(submission?.presentationDefinitions).toHaveLength(1) + expect(submission?.submissionData.definition_id).toBe('OpenBadgeCredential') + expect(submission?.presentations).toHaveLength(1) + expect(submission?.presentations[0].vcs[0].type).toContain('OpenBadgeCredential') }) // it('edited walt vp request', async () => { From eb58ad9035ddec5930579ef809ee887d10586d25 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Sun, 12 Nov 2023 18:32:01 +0100 Subject: [PATCH 038/115] fix: check VpFormat, fix test Signed-off-by: Martin Auer --- .../src/OpenId4VcHolderServiceOptions.ts | 2 + .../PresentationExchangeService.ts | 175 +++++++++++++----- .../tests/openid4vp-holder.e2e.test.ts | 2 +- 3 files changed, 136 insertions(+), 43 deletions(-) diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts index 99179f4152..6960a54bca 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts @@ -23,6 +23,8 @@ export type ProofSubmissionResponse = { submittedResponse: AuthorizationResponsePayload } +export type VpFormat = 'jwt_vp' | 'ldp_vp' + /** * The credential formats that are supported by the openid4vc holder */ diff --git a/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts b/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts index 183a2eb90b..7d6b54a054 100644 --- a/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts +++ b/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts @@ -1,4 +1,5 @@ import type { InputDescriptorToCredentials, PresentationSubmission } from './selection/types' +import type { VpFormat, VpFormat } from '../OpenId4VcHolderServiceOptions' import type { AgentContext, Query, @@ -16,7 +17,9 @@ import type { PresentationDefinitionV1, PresentationSubmission as PexPresentationSubmission, Descriptor, + InputDescriptorV2, } from '@sphereon/pex-models' +import type { OriginalVerifiableCredential } from '@sphereon/ssi-types' import { AriesFrameworkError, @@ -127,6 +130,40 @@ export class PresentationExchangeService { subjectsToInputDescriptors[subjectId] = inputDescriptorsToCredentials } + private getPresentationFormat( + presentationDefinition: IPresentationDefinition, + credentials: OriginalVerifiableCredential[] + ): VpFormat { + const allCredentialsAreJwtVc = credentials?.every((c) => typeof c === 'string') + const allCredentialsAreLdpVc = credentials?.every((c) => typeof c !== 'string') + + const inputDescriptorsNotSupportingJwtVc = (presentationDefinition.input_descriptors as InputDescriptorV2[]).filter( + (d) => d.format && d.format.jwt_vc === undefined + ) + + const inputDescriptorsNotSupportingLdpVc = (presentationDefinition.input_descriptors as InputDescriptorV2[]).filter( + (d) => d.format && d.format.ldp_vc === undefined + ) + + if ( + allCredentialsAreJwtVc && + (presentationDefinition.format === undefined || presentationDefinition.format.jwt_vc) && + inputDescriptorsNotSupportingJwtVc.length === 0 + ) { + return 'jwt_vp' + } else if ( + allCredentialsAreLdpVc && + (presentationDefinition.format === undefined || presentationDefinition.format.ldp_vc) && + inputDescriptorsNotSupportingLdpVc.length === 0 + ) { + return 'ldp_vp' + } else { + throw new AriesFrameworkError( + 'No suitable presentation format found for the given presentation definition, and credentials' + ) + } + } + public async createPresentation( agentContext: AgentContext, options: { @@ -152,7 +189,10 @@ export class PresentationExchangeService { }) }) - const verifiablePresentationResults: VerifiablePresentationResult[] = [] + const verifiablePresentationResultsWithFormat: { + verifiablePresentationResult: VerifiablePresentationResult + format: VpFormat + }[] = [] const subjectToInputDescriptors = Object.entries(proofStructure) for (const [subjectId, subjectInputDescriptorsToCredentials] of subjectToInputDescriptors) { @@ -184,12 +224,14 @@ export class PresentationExchangeService { submission_requirements: undefined, } - // TODO: Q1: is holder always subject id, what if there are multiple subjects??? - // TODO: Q2: What about proofType, proofPurpose verification method for multiple subjects? + const format = this.getPresentationFormat(presentationDefinitionForSubject, credentialsForSubject) + + // FIXME: Q1: is holder always subject id, what if there are multiple subjects??? + // FIXME: Q2: What about proofType, proofPurpose verification method for multiple subjects? const verifiablePresentationResult = await this.pex.verifiablePresentationFrom( presentationDefinitionForSubject, credentialsForSubject, - this.getPresentationSignCallback(agentContext, verificationMethod), + this.getPresentationSignCallback(agentContext, verificationMethod, format), { holderDID: subjectId, proofOptions: { challenge, domain, nonce }, @@ -197,34 +239,36 @@ export class PresentationExchangeService { } ) - verifiablePresentationResults.push(verifiablePresentationResult) + verifiablePresentationResultsWithFormat.push({ verifiablePresentationResult, format }) } - if (subjectToInputDescriptors.length !== verifiablePresentationResults.length) { - if (!verifiablePresentationResults[0]) throw new AriesFrameworkError('No verifiable presentations created.') + if (!verifiablePresentationResultsWithFormat[0]) { + throw new AriesFrameworkError('No verifiable presentations created.') + } + + if (subjectToInputDescriptors.length !== verifiablePresentationResultsWithFormat.length) { throw new AriesFrameworkError('Invalid amount of verifiable presentations created.') } const presentationSubmission: PexPresentationSubmission = { - id: verifiablePresentationResults[0].presentationSubmission.id, - definition_id: verifiablePresentationResults[0].presentationSubmission.definition_id, + id: verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.id, + definition_id: + verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.definition_id, descriptor_map: [], } - for (const vp of verifiablePresentationResults) { + for (const [index, vp] of verifiablePresentationResultsWithFormat.entries()) { presentationSubmission.descriptor_map.push( - ...vp.presentationSubmission.descriptor_map.map((descriptor): Descriptor => { - const index = verifiablePresentationResults.indexOf(vp) - const prefix = verifiablePresentationResults.length > 1 ? `$[${index}]` : '$' - // TODO: use enum instead opf jwt_vp | jwt_vc_json + ...vp.verifiablePresentationResult.presentationSubmission.descriptor_map.map((descriptor): Descriptor => { + const prefix = verifiablePresentationResultsWithFormat.length > 1 ? `$[${index}]` : '$' return { - format: 'jwt_vp', + format: vp.format, path: prefix, id: descriptor.id, path_nested: { ...descriptor, path: descriptor.path.replace('$.', `${prefix}.vp.`), - format: 'jwt_vc_json', + format: 'jwt_vc_json', // TODO: why jwt_vc_json }, } }) @@ -232,11 +276,12 @@ export class PresentationExchangeService { } return { - verifiablePresentations: verifiablePresentationResults.map((r) => - getW3cVerifiablePresentationInstance(r.verifiablePresentation) + verifiablePresentations: verifiablePresentationResultsWithFormat.map((r) => + getW3cVerifiablePresentationInstance(r.verifiablePresentationResult.verifiablePresentation) ), presentationSubmission, - presentationSubmissionLocation: verifiablePresentationResults[0].presentationSubmissionLocation, + presentationSubmissionLocation: + verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmissionLocation, } } @@ -265,22 +310,61 @@ export class PresentationExchangeService { return alg } + private getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( + algorithmsSatisfyingDefinition: string[], + inputDescriptorAlgorithms: string[][] + ) { + const allDescriptorAlgorithms = inputDescriptorAlgorithms.flat() + const algorithmsSatisfyingDescriptors = allDescriptorAlgorithms.filter((alg) => + inputDescriptorAlgorithms.every((descriptorAlgorithmSet) => descriptorAlgorithmSet.includes(alg)) + ) + + const algorithmsSatisfyingPdAndDescriptorRestrictions = algorithmsSatisfyingDefinition.filter((alg) => + algorithmsSatisfyingDescriptors.includes(alg) + ) + + if ( + algorithmsSatisfyingDefinition.length > 0 && + algorithmsSatisfyingDescriptors.length > 0 && + algorithmsSatisfyingPdAndDescriptorRestrictions.length === 0 + ) { + throw new AriesFrameworkError( + `No signature algorithm found for satisfying restrictions of the presentation definition and input descriptors.` + ) + } + + if (allDescriptorAlgorithms.length > 0 && algorithmsSatisfyingDescriptors.length === 0) { + throw new AriesFrameworkError( + `No signature algorithm found for satisfying restrictions of the input descriptors.` + ) + } + + let suitableAlgorithms: string[] | undefined = undefined + if (algorithmsSatisfyingPdAndDescriptorRestrictions.length > 0) { + suitableAlgorithms = algorithmsSatisfyingPdAndDescriptorRestrictions + } else if (algorithmsSatisfyingDescriptors.length > 0) { + suitableAlgorithms = algorithmsSatisfyingDescriptors + } else if (algorithmsSatisfyingDefinition.length > 0) { + suitableAlgorithms = algorithmsSatisfyingDefinition + } + + return suitableAlgorithms + } + private getSigningAlgorithmForJwtVc( presentationDefinition: IPresentationDefinition, verificationMethod: VerificationMethod ) { - const suitableAlgorithms = presentationDefinition.format?.jwt_vc?.alg - // const inputDescriptors: InputDescriptorV2[] = presentationDefinition.input_descriptors as InputDescriptorV2[] - - // TODO: continue - - // const inputDescriptorAlgorithms: string[][] = inputDescriptors - // .map((inputDescriptor) => inputDescriptor.format?.jwt_vc?.alg) - // .filter((alg): alg is string[] => alg !== undefined && alg.length === 0) + const algorithmsSatisfyingDefinition = presentationDefinition.format?.jwt_vc?.alg || [] - // const allInputDescriptorAlgorithms = inputDescriptorAlgorithms.flat() + const inputDescriptorAlgorithms: string[][] = presentationDefinition.input_descriptors + .map((descriptor) => (descriptor as InputDescriptorV2).format?.jwt_vc?.alg || []) + .filter((alg) => alg.length > 0) - // const isAlgInEveryInputDescriptor = inputDescriptorAlgorithms.every((alg, _, arr) => arr.includes(alg)) + const suitableAlgorithms = this.getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( + algorithmsSatisfyingDefinition, + inputDescriptorAlgorithms + ) return this.getSigningAlgorithmFromVerificationMethod(verificationMethod, suitableAlgorithms) } @@ -289,17 +373,27 @@ export class PresentationExchangeService { presentationDefinition: IPresentationDefinition, verificationMethod: VerificationMethod ) { - const suitableSignaturesSuites = presentationDefinition.format?.ldp_vc?.proof_type + const algorithmsSatisfyingDefinition = presentationDefinition.format?.ldp_vc?.proof_type || [] + + const inputDescriptorAlgorithms: string[][] = presentationDefinition.input_descriptors + .map((descriptor) => (descriptor as InputDescriptorV2).format?.ldp_vc?.proof_type || []) + .filter((alg) => alg.length > 0) + + const suitableAlgorithms = this.getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( + algorithmsSatisfyingDefinition, + inputDescriptorAlgorithms + ) + // TODO: find out which signature suites are supported by the verification method // TODO: check if a supported signature suite is in the list of suitable signature suites - // TODO: remake this after - if (!suitableSignaturesSuites || suitableSignaturesSuites.length === 0) - throw new AriesFrameworkError(`No suitable signature suite found for presentation definition.`) - - return suitableSignaturesSuites[0] + return suitableAlgorithms ? suitableAlgorithms[0] : 'todo' } - public getPresentationSignCallback(agentContext: AgentContext, verificationMethod: VerificationMethod) { + public getPresentationSignCallback( + agentContext: AgentContext, + verificationMethod: VerificationMethod, + vpFormat: VpFormat + ) { const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) return async (callBackParams: PresentationSignCallBackParams) => { @@ -314,14 +408,11 @@ export class PresentationExchangeService { ) } - const allJwt = presentationJson.verifiableCredential?.every((c) => typeof c === 'string') - const allJsonLd = presentationJson.verifiableCredential?.every((c) => typeof c !== 'string') - // Clients MUST ignore any presentation_submission element included inside a Verifiable Presentation. const presentationToSign = { ...presentationJson, presentation_submission: undefined } let signedPresentation: W3cVerifiablePresentation - if (allJwt) { + if (vpFormat === 'jwt_vp') { signedPresentation = await w3cCredentialService.signPresentation(agentContext, { format: ClaimFormat.JwtVp, verificationMethod: verificationMethod.id, @@ -330,11 +421,11 @@ export class PresentationExchangeService { challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), domain, }) - } else if (allJsonLd) { + } else if (vpFormat === 'ldp_vp') { signedPresentation = await w3cCredentialService.signPresentation(agentContext, { format: ClaimFormat.LdpVp, proofType: this.getSigningAlgorithmForLdpVc(presentationDefinition, verificationMethod), - proofPurpose: 'assertionMethod', // TODO: authentication + proofPurpose: 'authentication', verificationMethod: verificationMethod.id, presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation), challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), diff --git a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts index db1b3f4eb8..e36a3f3d46 100644 --- a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts @@ -508,7 +508,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { expect(submission?.presentationDefinitions).toHaveLength(1) expect(submission?.submissionData.definition_id).toBe('OpenBadgeCredential') expect(submission?.presentations).toHaveLength(1) - expect(submission?.presentations[0].vcs[0].type).toContain('OpenBadgeCredential') + expect(submission?.presentations[0].vcs[0].credential.type).toContain('OpenBadgeCredential') }) // it('edited walt vp request', async () => { From e1d6c70a2825bb8e6534f32273415bd503c588e4 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Sun, 12 Nov 2023 18:35:45 +0100 Subject: [PATCH 039/115] fix: import statement in PresentationExchangeService.ts Signed-off-by: Martin Auer --- .../src/presentations/PresentationExchangeService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts b/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts index 7d6b54a054..b868548103 100644 --- a/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts +++ b/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts @@ -1,5 +1,5 @@ import type { InputDescriptorToCredentials, PresentationSubmission } from './selection/types' -import type { VpFormat, VpFormat } from '../OpenId4VcHolderServiceOptions' +import type { VpFormat } from '../OpenId4VcHolderServiceOptions' import type { AgentContext, Query, From 055605d498cdd1baed2f8a2d161f0d51128c8226 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 13 Nov 2023 08:55:01 +0100 Subject: [PATCH 040/115] feat: create getProofTypeForLdpVc method and use Pex to create submission descriptors Signed-off-by: Martin Auer --- .../PresentationExchangeService.ts | 68 ++++++++++++------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts b/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts index b868548103..100f3d631e 100644 --- a/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts +++ b/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts @@ -32,8 +32,9 @@ import { W3cCredentialService, W3cPresentation, W3cCredentialRepository, + SignatureSuiteRegistry, } from '@aries-framework/core' -import { PEVersion, PEX } from '@sphereon/pex' +import { PEVersion, PEX, PresentationSubmissionLocation } from '@sphereon/pex' import { selectCredentialsForRequest } from './selection/PexCredentialSelection' import { @@ -220,7 +221,6 @@ export class PresentationExchangeService { input_descriptors: inputDescriptorsForSubject, // We remove the submission requirements, as it will otherwise fail to create the VP - // TODO: Will this cause issue for creating the credential? Need to run tests submission_requirements: undefined, } @@ -236,6 +236,7 @@ export class PresentationExchangeService { holderDID: subjectId, proofOptions: { challenge, domain, nonce }, signatureOptions: { verificationMethod: verificationMethod?.id }, + presentationSubmissionLocation: PresentationSubmissionLocation.EXTERNAL, } ) @@ -246,10 +247,15 @@ export class PresentationExchangeService { throw new AriesFrameworkError('No verifiable presentations created.') } + if (!verifiablePresentationResultsWithFormat[0]) { + throw new AriesFrameworkError('No verifiable presentations created.') + } + if (subjectToInputDescriptors.length !== verifiablePresentationResultsWithFormat.length) { throw new AriesFrameworkError('Invalid amount of verifiable presentations created.') } + verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission const presentationSubmission: PexPresentationSubmission = { id: verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.id, definition_id: @@ -257,22 +263,9 @@ export class PresentationExchangeService { descriptor_map: [], } - for (const [index, vp] of verifiablePresentationResultsWithFormat.entries()) { - presentationSubmission.descriptor_map.push( - ...vp.verifiablePresentationResult.presentationSubmission.descriptor_map.map((descriptor): Descriptor => { - const prefix = verifiablePresentationResultsWithFormat.length > 1 ? `$[${index}]` : '$' - return { - format: vp.format, - path: prefix, - id: descriptor.id, - path_nested: { - ...descriptor, - path: descriptor.path.replace('$.', `${prefix}.vp.`), - format: 'jwt_vc_json', // TODO: why jwt_vc_json - }, - } - }) - ) + for (const vpf of verifiablePresentationResultsWithFormat) { + const { verifiablePresentationResult } = vpf + presentationSubmission.descriptor_map.push(...verifiablePresentationResult.presentationSubmission.descriptor_map) } return { @@ -369,7 +362,9 @@ export class PresentationExchangeService { return this.getSigningAlgorithmFromVerificationMethod(verificationMethod, suitableAlgorithms) } - private getSigningAlgorithmForLdpVc( + // TODO: is this a proper implementation? + private getProofTypeForLdpVc( + agentContext: AgentContext, presentationDefinition: IPresentationDefinition, verificationMethod: VerificationMethod ) { @@ -379,14 +374,37 @@ export class PresentationExchangeService { .map((descriptor) => (descriptor as InputDescriptorV2).format?.ldp_vc?.proof_type || []) .filter((alg) => alg.length > 0) - const suitableAlgorithms = this.getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( + const suitableSignatureSuites = this.getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( algorithmsSatisfyingDefinition, inputDescriptorAlgorithms ) - // TODO: find out which signature suites are supported by the verification method - // TODO: check if a supported signature suite is in the list of suitable signature suites - return suitableAlgorithms ? suitableAlgorithms[0] : 'todo' + // For each of the supported algs, find the key types, then find the proof types + const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) + + const supportedSignatureSuite = signatureSuiteRegistry.getByVerificationMethodType(verificationMethod.type) + if (!supportedSignatureSuite) { + throw new AriesFrameworkError( + `Couldn't find a supported signature suite for the given verification method type '${verificationMethod.type}'.` + ) + } + + if (suitableSignatureSuites) { + if (suitableSignatureSuites.includes(supportedSignatureSuite.proofType) === false) { + throw new AriesFrameworkError( + [ + 'No possible signature suite found for the given verification method.', + `Verification method type: ${verificationMethod.type}`, + `SupportedSignatureSuite '${supportedSignatureSuite.proofType}'`, + `SuitableSignatureSuites: ${suitableSignatureSuites.join(', ')}`, + ].join('\n') + ) + } + + return supportedSignatureSuite.proofType + } + + return supportedSignatureSuite.proofType } public getPresentationSignCallback( @@ -415,16 +433,16 @@ export class PresentationExchangeService { if (vpFormat === 'jwt_vp') { signedPresentation = await w3cCredentialService.signPresentation(agentContext, { format: ClaimFormat.JwtVp, + alg: this.getSigningAlgorithmForJwtVc(presentationDefinition, verificationMethod), verificationMethod: verificationMethod.id, presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation), - alg: this.getSigningAlgorithmForJwtVc(presentationDefinition, verificationMethod), challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), domain, }) } else if (vpFormat === 'ldp_vp') { signedPresentation = await w3cCredentialService.signPresentation(agentContext, { format: ClaimFormat.LdpVp, - proofType: this.getSigningAlgorithmForLdpVc(presentationDefinition, verificationMethod), + proofType: this.getProofTypeForLdpVc(agentContext, presentationDefinition, verificationMethod), proofPurpose: 'authentication', verificationMethod: verificationMethod.id, presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation), From 6a3dd634373c0b7d5e39043b67974758b109879d Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 15 Nov 2023 15:06:12 +0100 Subject: [PATCH 041/115] feat: add openid4vc issuer support Signed-off-by: Martin Auer --- .../vc/jwt-vc/W3cJwtCredentialService.ts | 2 +- .../PresentationExchangeService.ts | 1 - .../tests/OpenId4VcHolderModule.test.ts | 2 +- packages/openid4vc-issuer/package.json | 4 +- .../src/OpenId4VcIssuerApi.ts | 56 +- .../src/OpenId4VcIssuerModule.ts | 10 + .../src/OpenId4VcIssuerModuleConfig.ts | 33 ++ .../src/OpenId4VcIssuerService.ts | 463 ++++++++++++++- .../src/OpenId4VcIssuerServiceOptions.ts | 65 +- packages/openid4vc-issuer/src/index.ts | 2 + .../tests/OpenId4VcIssuerModule.test.ts | 41 ++ .../tests/openid4vc-issuer.e2e.test.ts | 556 +++++++++++++++++- .../src/OpenId4VcVerifierService.ts | 2 +- .../tests/OpenId4VcVerifierModule.test.ts | 26 + yarn.lock | 19 +- 15 files changed, 1231 insertions(+), 51 deletions(-) create mode 100644 packages/openid4vc-issuer/src/OpenId4VcIssuerModuleConfig.ts create mode 100644 packages/openid4vc-issuer/tests/OpenId4VcIssuerModule.test.ts create mode 100644 packages/openid4vc-verifier/tests/OpenId4VcVerifierModule.test.ts diff --git a/packages/core/src/modules/vc/jwt-vc/W3cJwtCredentialService.ts b/packages/core/src/modules/vc/jwt-vc/W3cJwtCredentialService.ts index 87bf4b476c..0dedea6879 100644 --- a/packages/core/src/modules/vc/jwt-vc/W3cJwtCredentialService.ts +++ b/packages/core/src/modules/vc/jwt-vc/W3cJwtCredentialService.ts @@ -509,7 +509,7 @@ export class W3cJwtCredentialService { verificationMethod = didDocument.dereferenceKey(kid, purpose) if (signerId && didDocument.id !== signerId) { - throw new AriesFrameworkError(`kid '${kid}' does not match id of signer (holder/issuer) '${signerId}'`) + throw new AriesFrameworkError(`kid '${kid}' does not match id of signer (holder/issuer) '${didDocument.id}'`) } } else { if (!signerId) { diff --git a/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts b/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts index 100f3d631e..55e99c4fe6 100644 --- a/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts +++ b/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts @@ -16,7 +16,6 @@ import type { import type { PresentationDefinitionV1, PresentationSubmission as PexPresentationSubmission, - Descriptor, InputDescriptorV2, } from '@sphereon/pex-models' import type { OriginalVerifiableCredential } from '@sphereon/ssi-types' diff --git a/packages/openid4vc-holder/tests/OpenId4VcHolderModule.test.ts b/packages/openid4vc-holder/tests/OpenId4VcHolderModule.test.ts index f138572705..3540ef8503 100644 --- a/packages/openid4vc-holder/tests/OpenId4VcHolderModule.test.ts +++ b/packages/openid4vc-holder/tests/OpenId4VcHolderModule.test.ts @@ -13,7 +13,7 @@ const dependencyManager = { resolve: jest.fn().mockReturnValue({ logger: { warn: jest.fn() } }), } as unknown as DependencyManager -describe('OpenId4VcClientModule', () => { +describe('OpenId4VcHolderModule', () => { test('registers dependencies on the dependency manager', () => { const openId4VcClientModule = new OpenId4VcHolderModule() openId4VcClientModule.register(dependencyManager) diff --git a/packages/openid4vc-issuer/package.json b/packages/openid4vc-issuer/package.json index 79714a77c5..bb9787ad78 100644 --- a/packages/openid4vc-issuer/package.json +++ b/packages/openid4vc-issuer/package.json @@ -25,8 +25,8 @@ }, "dependencies": { "@aries-framework/core": "0.4.2", - "@sphereon/oid4vci-issuer": "^0.7.3", - "@sphereon/oid4vci-common": "^0.7.3", + "@sphereon/oid4vci-issuer": "^0.8.1", + "@sphereon/oid4vci-common": "^0.8.1", "@sphereon/ssi-types": "^0.17.5" }, "devDependencies": { diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts index feaff0ba8e..212d36c15f 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts @@ -1,4 +1,10 @@ -import type { IssueCredentialOptions, SendCredentialOfferOptions } from './OpenId4VcIssuerServiceOptions' +import type { + IssueCredentialOptions, + CreateCredentialOfferOptions, + CredentialOfferAndRequest, + OfferedCredential, +} from './OpenId4VcIssuerServiceOptions' +import type { CredentialOfferPayload } from '@sphereon/oid4vci-common' import { injectable, AgentContext } from '@aries-framework/core' @@ -6,6 +12,9 @@ import { OpenId4VcIssuerService } from './OpenId4VcIssuerService' /** * @public + * This class represents the API for interacting with the OpenID4VC Issuer service. + * It provides methods for creating a credential offer, creating a response to a credential issuance request, + * and retrieving a credential offer from a URI. */ @injectable() export class OpenId4VcIssuerApi { @@ -17,11 +26,48 @@ export class OpenId4VcIssuerApi { this.openId4VcIssuerService = openId4VcIssuerService } - public sendCredentialOffer(options: SendCredentialOfferOptions) { - // TODO: Implement + /** + * Creates a credential offer, and credential offer request. + * Either the preAuthorizedCodeFlowConfig or the authorizationCodeFlowConfig must be provided. + * + * @param {OfferedCredential[]} offeredCredentials - The credentials to be offered. + * @param {IssuerMetadata} options.issuerMetadata - Metadata about the issuer. + * @param {string} options.credentialOfferUri - The URI to retrieve the credential offer if the offer is passed by reference. + * @param {string} options.scheme - The credential offer request scheme. Default is https. + * @param {string} options.baseUri - The base URI of the credential offer request. + * @param {PreAuthorizedCodeFlowConfig} options.preAuthorizedCodeFlowConfig - The configuration for the pre-authorized code flow. This or the authorizationCodeFlowConfig must be provided. + * @param {AuthorizationCodeFlowConfig} options.authorizationCodeFlowConfig - The configuration for the authorization code flow. This or the preAuthorizedCodeFlowConfig must be provided. + * + * @returns {CredentialOfferAndRequest} Object containing the payload of the credential offer and the credential offer request, which is to be sent to the wallet. + */ + public async createCredentialOffer( + offeredCredentials: OfferedCredential[], + options: CreateCredentialOfferOptions + ): Promise { + return await this.openId4VcIssuerService.createCredentialOffer(this.agentContext, offeredCredentials, options) } - public issueCredential(options: IssueCredentialOptions) { - // TODO: Implement + /** + * This function retrieves a credential offer from a given URI. + * Retrieving a credential offer from a URI is possible after a credential offer was created with + * @see createCredentialOffer and the credentialOfferUri option. + * + * @throws if no credential offer can found for the given URI. + * @param {string} uri - The URI for which to retrieve the credential offer. + * @returns {CredentialOfferPayload} - The credential offer payload associated with the given URI. + */ + public async getCredentialOfferFromUri(uri: string): Promise { + return await this.openId4VcIssuerService.getCredentialOfferFromUri(uri) + } + + /** + * This function creates a response which can be send to the holder after receiving a credential issuance request. + * + * @param {string} options.credentialRequest - The credential request, for which to create a response. + * @param {string} options.credential - The credential to be issued. + * @param {IssuerMetadata} options.issuerMetadata - Metadata about the issuer. + */ + public async createIssueCredentialResponse(options: IssueCredentialOptions) { + return await this.openId4VcIssuerService.createIssueCredentialResponse(this.agentContext, options) } } diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerModule.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerModule.ts index 26a65a88ad..ab7daa8ec6 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerModule.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerModule.ts @@ -1,8 +1,10 @@ +import type { OpenId4VcIssuerModuleConfigOptions } from './OpenId4VcIssuerModuleConfig' import type { DependencyManager, Module } from '@aries-framework/core' import { AgentConfig } from '@aries-framework/core' import { OpenId4VcIssuerApi } from './OpenId4VcIssuerApi' +import { OpenId4VcIssuerModuleConfig } from './OpenId4VcIssuerModuleConfig' import { OpenId4VcIssuerService } from './OpenId4VcIssuerService' /** @@ -10,6 +12,11 @@ import { OpenId4VcIssuerService } from './OpenId4VcIssuerService' */ export class OpenId4VcIssuerModule implements Module { public readonly api = OpenId4VcIssuerApi + public readonly config: OpenId4VcIssuerModuleConfig + + public constructor(options: OpenId4VcIssuerModuleConfigOptions) { + this.config = new OpenId4VcIssuerModuleConfig(options) + } /** * Registers the dependencies of the question answer module on the dependency manager. @@ -22,6 +29,9 @@ export class OpenId4VcIssuerModule implements Module { "The '@aries-framework/openid4vc-issuer' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." ) + // Register config + dependencyManager.registerInstance(OpenId4VcIssuerModuleConfig, this.config) + // Api dependencyManager.registerContextScoped(OpenId4VcIssuerApi) diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerModuleConfig.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerModuleConfig.ts new file mode 100644 index 0000000000..c132b5f0a6 --- /dev/null +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerModuleConfig.ts @@ -0,0 +1,33 @@ +import type { IssuerMetadata } from './OpenId4VcIssuerServiceOptions' +import type { CNonceState, CredentialOfferSession, IStateManager, URIState } from '@sphereon/oid4vci-common' + +export interface OpenId4VcIssuerModuleConfigOptions { + issuerMetadata: IssuerMetadata + cNonceStateManager?: IStateManager + credentialOfferSessionManager?: IStateManager + uriStateManager?: IStateManager +} + +export class OpenId4VcIssuerModuleConfig { + private options: OpenId4VcIssuerModuleConfigOptions + + public constructor(options: OpenId4VcIssuerModuleConfigOptions) { + this.options = options + } + + public get issuerMetadata() { + return this.options.issuerMetadata + } + + public get cNonceStateManager() { + return this.options.cNonceStateManager + } + + public get credentialOfferSessionManager() { + return this.options.credentialOfferSessionManager + } + + public get uriStateManager() { + return this.options.uriStateManager + } +} diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts index 7a50ba6366..b89aa12a0c 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts @@ -1,12 +1,77 @@ +import type { + IssueCredentialOptions, + CreateCredentialOfferOptions, + AuthorizationCodeFlowConfig, + PreAuthorizedCodeFlowConfig, + OfferedCredential, + IssuerMetadata, +} from './OpenId4VcIssuerServiceOptions' +import type { + AgentContext, + VerificationMethod, + W3cVerifiableCredential, + DidDocument, + JwaSignatureAlgorithm, +} from '@aries-framework/core' +import type { + CredentialRequestJwtVc, + CredentialRequestLdpVc, + Grant, + CredentialSupported, + MetadataDisplay, + JWTVerifyCallback, + CredentialOfferFormat, + CredentialRequestV1_0_11, + CredentialOfferPayloadV1_0_11, + IStateManager, + CNonceState, + CredentialOfferSession, + URIState, +} from '@sphereon/oid4vci-common' +import type { + CredentialDataSupplier, + CredentialDataSupplierArgs, + CredentialSignerCallback, +} from '@sphereon/oid4vci-issuer' +import type { ICredential, W3CVerifiableCredential as SphereonW3cVerifiableCredential } from '@sphereon/ssi-types' + import { + AriesFrameworkError, + ClaimFormat, InjectionSymbols, JwsService, Logger, - W3cCredentialRepository, W3cCredentialService, inject, injectable, + JsonTransformer, + W3cCredential, + Jwt, + SignatureSuiteRegistry, + DidsApi, + getKeyFromVerificationMethod, + getJwkFromKey, + equalsIgnoreOrder, } from '@aries-framework/core' +import { IssueStatus } from '@sphereon/oid4vci-common' +import { MemoryStates, VcIssuerBuilder } from '@sphereon/oid4vci-issuer' + +import { OpenId4VcIssuerModuleConfig } from './OpenId4VcIssuerModuleConfig' + +// TODO: duplicate +function getSphereonW3cVerifiableCredential( + w3cVerifiableCredential: W3cVerifiableCredential +): SphereonW3cVerifiableCredential { + if (w3cVerifiableCredential.claimFormat === ClaimFormat.LdpVc) { + return JsonTransformer.toJSON(w3cVerifiableCredential) as SphereonW3cVerifiableCredential + } else if (w3cVerifiableCredential.claimFormat === ClaimFormat.JwtVc) { + return w3cVerifiableCredential.serializedJwt + } else { + throw new AriesFrameworkError( + `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` + ) + } +} /** * @internal @@ -15,18 +80,406 @@ import { export class OpenId4VcIssuerService { private logger: Logger private w3cCredentialService: W3cCredentialService - private w3cCredentialRepository: W3cCredentialRepository private jwsService: JwsService + private cNonceExpiresIn: number = 5 * 60 * 1000 // 5 minutes + private tokenExpiresIn: number = 3 * 60 * 1000 // 3 minutes + private issuerMetadata: IssuerMetadata + private _cNonceStateManager: IStateManager + private _credentialOfferSessionManager: IStateManager + private _uriStateManager: IStateManager + + public get cNonceStateManager() { + return this._cNonceStateManager + } + + public get credentialOfferSessionManager() { + return this._credentialOfferSessionManager + } + + public get uriStateManager() { + return this._uriStateManager + } public constructor( @inject(InjectionSymbols.Logger) logger: Logger, + openId4VcIssuerModuleConfig: OpenId4VcIssuerModuleConfig, w3cCredentialService: W3cCredentialService, - w3cCredentialRepository: W3cCredentialRepository, jwsService: JwsService ) { this.w3cCredentialService = w3cCredentialService - this.w3cCredentialRepository = w3cCredentialRepository - this.jwsService = jwsService this.logger = logger + this.issuerMetadata = openId4VcIssuerModuleConfig.issuerMetadata + this.jwsService = jwsService + this._cNonceStateManager = openId4VcIssuerModuleConfig.cNonceStateManager ?? new MemoryStates() + this._credentialOfferSessionManager = + openId4VcIssuerModuleConfig.credentialOfferSessionManager ?? new MemoryStates() + this._uriStateManager = openId4VcIssuerModuleConfig.uriStateManager ?? new MemoryStates() + } + + // TODO: check if this is correct + private getProofTypeForLdpVc(agentContext: AgentContext, verificationMethod: VerificationMethod) { + const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) + + const supportedSignatureSuite = signatureSuiteRegistry.getByVerificationMethodType(verificationMethod.type) + if (!supportedSignatureSuite) { + throw new AriesFrameworkError( + `Couldn't find a supported signature suite for the given verification method type '${verificationMethod.type}'.` + ) + } + + return supportedSignatureSuite.proofType + } + + private getJwtVerifyCallback = (agentContext: AgentContext): JWTVerifyCallback => { + return async (opts) => { + const { jwt } = opts + + const { header, payload } = Jwt.fromSerializedJwt(jwt) + const { alg, kid } = header + + // kid: JOSE Header containing the key ID. If the Credential shall be bound to a DID, + // the kid refers to a DID URL which identifies a particular key in the DID Document that + // the Credential shall be bound to. MUST NOT be present if jwk or x5c is present. + if (!kid) throw new AriesFrameworkError('No KID is present for verifying the proof of possession.') + + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const didDocument = await didsApi.resolveDidDocument(kid) + const verificationMethod = didDocument.dereferenceKey(kid, ['authentication', 'assertionMethod']) + const key = getKeyFromVerificationMethod(verificationMethod) + const jwk = getJwkFromKey(key) + + if (!jwk.supportsSignatureAlgorithm(alg)) { + throw new AriesFrameworkError( + `The signature algorithm '${alg}' is not supported by keys of type '${jwk.keyType}'.` + ) + } + + const { isValid } = await this.jwsService.verifyJws(agentContext, { + jws: jwt, + jwkResolver: () => jwk, + }) + + if (!isValid) throw new AriesFrameworkError('Could not verify JWT signature.') + + return { + jwt: { header, payload: payload.toJson() }, + kid, + did: didDocument.id, + alg, + didDocument, + } + } + } + + private getCredentialSigningCallback = ( + agentContext: AgentContext, + issuerVerificationMethod: VerificationMethod + ): CredentialSignerCallback => { + return async (opts) => { + const { credential, jwtVerifyResult, format } = opts + + const { alg, kid, didDocument: holderDidDocument } = jwtVerifyResult + + if (!kid) throw new AriesFrameworkError('No KID present for binding the credential to a holder.') + if (!holderDidDocument) { + throw new AriesFrameworkError('No DID document present for binding the credential to a holder.') + } + + // If the Credential shall be bound to a DID, the kid refers to a DID URL which identifies a + // particular key in the DID Document that the Credential shall be bound to. + // TODO: proofpurpose + const holderVerificationMethod = holderDidDocument.dereferenceKey(kid, ['assertionMethod', 'assertionMethod']) + + let signed: W3cVerifiableCredential + if (format === 'jwt_vc_json' || format === 'jwt_vc_json-ld') { + signed = await this.w3cCredentialService.signCredential(agentContext, { + format: ClaimFormat.JwtVc, + credential: W3cCredential.fromJson(credential), + verificationMethod: issuerVerificationMethod.id, + alg: alg as JwaSignatureAlgorithm, + }) + } else { + signed = await this.w3cCredentialService.signCredential(agentContext, { + format: ClaimFormat.LdpVc, + credential: W3cCredential.fromJson(credential), + verificationMethod: issuerVerificationMethod.id, + proofPurpose: 'authentication', // TODO: is it authentication? + proofType: this.getProofTypeForLdpVc(agentContext, holderVerificationMethod), + }) + } + + return getSphereonW3cVerifiableCredential(signed) + } + } + + private getVcIssuer( + agentContext: AgentContext, + options: { + credentialIssuer: string + credentialEndpoint: string + tokenEndpoint: string + credentialsSupported: CredentialSupported[] + authorizationServer?: string + issuerDisplay?: MetadataDisplay | MetadataDisplay[] + } + ) { + const { credentialIssuer, tokenEndpoint, credentialEndpoint, credentialsSupported } = options + const builder = new VcIssuerBuilder() + .withCredentialIssuer(credentialIssuer) + .withCredentialEndpoint(credentialEndpoint) + .withTokenEndpoint(tokenEndpoint) + .withCredentialsSupported(credentialsSupported) + .withCNonceExpiresIn(this.cNonceExpiresIn) // 5 minutes + .withCNonceStateManager(this._cNonceStateManager) + .withCredentialOfferStateManager(this._credentialOfferSessionManager) + .withCredentialOfferURIStateManager(this._uriStateManager) + .withJWTVerifyCallback(this.getJwtVerifyCallback(agentContext)) + .withCredentialSignerCallback(() => { + throw new AriesFrameworkError('this should never ba called') + }) + + if (options.authorizationServer) { + builder.withAuthorizationServer(options.authorizationServer) + } + + if (options.issuerDisplay) { + builder.withIssuerDisplay(options.issuerDisplay) + } + + return builder.build() + } + + private getGrantsFromConfig( + preAuthorizedCodeFlowConfig?: PreAuthorizedCodeFlowConfig, + authorizationCodeFlowConfig?: AuthorizationCodeFlowConfig + ) { + if (!preAuthorizedCodeFlowConfig && !authorizationCodeFlowConfig) { + throw new AriesFrameworkError( + `Either preAuthorizedCodeFlowConfig or authorizationCodeFlowConfig must be provided.` + ) + } + + let grants: Grant = {} + if (preAuthorizedCodeFlowConfig) { + grants = { + ...grants, + 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { + /** + * REQUIRED. The code representing the Credential Issuer's authorization for the Wallet to obtain Credentials of a certain type. + */ + 'pre-authorized_code': preAuthorizedCodeFlowConfig.preAuthorizedCode, + /** + * OPTIONAL. Boolean value specifying whether the Credential Issuer expects presentation of a user PIN along with the Token Request + * in a Pre-Authorized Code Flow. Default is false. + */ + user_pin_required: preAuthorizedCodeFlowConfig.userPinRequired, + }, + } + } + + if (authorizationCodeFlowConfig) { + grants = { + ...grants, + authorization_code: { issuer_state: authorizationCodeFlowConfig.issuerState }, + } + } + + return grants + } + + private mapInlineCredentialOfferIdToCredentialSupported(id: string, credentialsSupported: CredentialSupported[]) { + const credentialSupported = credentialsSupported.find((cs) => cs.id === id) + if (!credentialSupported) throw new AriesFrameworkError(`Credential supported with id '${id}' not found.`) + + return credentialSupported + } + + private getCredentialMetadata( + credential: CredentialSupported | CredentialOfferFormat + ): CredentialRequestJwtVc | CredentialRequestLdpVc { + if (credential.format === 'jwt_vc_json' || credential.format === 'jwt_vc_json-ld') { + return { + format: credential.format, + types: credential.types, + } + } else { + // TODO: + throw new AriesFrameworkError('Unsupported credential format') + } + } + + private getOfferedCredentialsMetadata( + credentials: (CredentialOfferFormat | string)[], + credentialsSupported: CredentialSupported[] + ) { + const credentialsReferencingCredentialsSupported = credentials + .filter((credential): credential is string => typeof credential === 'string') + .map((credentialId) => this.mapInlineCredentialOfferIdToCredentialSupported(credentialId, credentialsSupported)) + .map((credentialSupported) => this.getCredentialMetadata(credentialSupported)) + + const inlineCredentialOffers = credentials + .filter((credential): credential is CredentialOfferFormat => typeof credential !== 'string') + .map((credential) => this.getCredentialMetadata(credential)) + + return [...credentialsReferencingCredentialsSupported, ...inlineCredentialOffers] + } + + public async createCredentialOffer( + agentContext: AgentContext, + offeredCredentials: OfferedCredential[], + options: CreateCredentialOfferOptions + ) { + const { preAuthorizedCodeFlowConfig, authorizationCodeFlowConfig } = options + + const issuerMetadata = options.issuerMetadata ?? this.issuerMetadata + + // this checks if the structure of the credentials is correct + // it throws an error if a offered credential cannot be found in the credentialsSupported + this.getOfferedCredentialsMetadata(offeredCredentials, issuerMetadata.credentialsSupported) + + const vcIssuer = this.getVcIssuer(agentContext, issuerMetadata) + + const { uri, session } = await vcIssuer.createCredentialOfferURI({ + grants: this.getGrantsFromConfig(preAuthorizedCodeFlowConfig, authorizationCodeFlowConfig), + credentials: offeredCredentials, + credentialOfferUri: options.credentialOfferUri, + scheme: options.scheme ?? 'https', + baseUri: options.baseUri, + // TODO: THIS IS WRONG HOW TO SPECIFY ldp_ creds? + // credentialDefinition, + }) + + return { + credentialOffer: session.credentialOffer.credential_offer, + credentialOfferRequest: uri, + } + } + + private async getCredentialOfferSessionFromUri(uri: string) { + const uriState = await this._uriStateManager.get(uri) + if (!uriState) throw new AriesFrameworkError(`Credential offer uri '${uri}' not found.`) + + const credentialOfferSessionId = uriState.preAuthorizedCode ?? uriState.issuerState + + if (!credentialOfferSessionId) { + throw new AriesFrameworkError( + `Credential offer uri '${uri}' is not associated with a preAuthorizedCode or issuerState.` + ) + } + + const credentialOfferSession = await this._credentialOfferSessionManager.get(credentialOfferSessionId) + if (!credentialOfferSession) + throw new AriesFrameworkError( + `Credential offer session for '${uri}' with id '${credentialOfferSessionId}' not found.` + ) + + return { credentialOfferSessionId, credentialOfferSession } + } + + public async getCredentialOfferFromUri(uri: string) { + const { credentialOfferSession, credentialOfferSessionId } = await this.getCredentialOfferSessionFromUri(uri) + + credentialOfferSession.lastUpdatedAt = +new Date() + credentialOfferSession.status = IssueStatus.OFFER_URI_RETRIEVED + await this._credentialOfferSessionManager.set(credentialOfferSessionId, credentialOfferSession) + + return credentialOfferSession.credentialOffer.credential_offer + } + + private findOfferedCredentialsMatchingRequest( + credentialOffer: CredentialOfferPayloadV1_0_11, + credentialRequest: CredentialRequestV1_0_11, + credentialsSupported: CredentialSupported[] + ) { + const offeredCredentials = this.getOfferedCredentialsMetadata(credentialOffer.credentials, credentialsSupported) + + return offeredCredentials.filter((offeredCredential) => { + if (credentialRequest.format === 'jwt_vc_json' && offeredCredential.format === 'jwt_vc_json') { + return equalsIgnoreOrder(offeredCredential.types, credentialRequest.types) + } else if (credentialRequest.format === 'jwt_vc_json-ld' && offeredCredential.format === 'jwt_vc_json-ld') { + return equalsIgnoreOrder(offeredCredential.types, credentialRequest.types) + } else if (credentialRequest.format === 'ldp_vc' && offeredCredential.format === 'ldp_vc') { + return equalsIgnoreOrder( + offeredCredential.credential_definition.types, + credentialRequest.credential_definition.types + ) + } else { + throw new AriesFrameworkError(`Unsupported credential format ${credentialRequest.format}.`) + } + }) + } + + private getCredentialDataSupplier = ( + agentContext: AgentContext, + credential: W3cCredential, + credentialsSupported: CredentialSupported[], + issuerVerificationMethod: VerificationMethod + ): CredentialDataSupplier => { + return async (args: CredentialDataSupplierArgs) => { + const { credentialRequest, credentialOffer } = args + + const offeredCredentialsMatchingRequest = this.findOfferedCredentialsMatchingRequest( + credentialOffer.credential_offer, + credentialRequest, + credentialsSupported + ) + + if (offeredCredentialsMatchingRequest.length === 0) { + throw new AriesFrameworkError('No offered credential matches the requested credential.') + } + + const issuedCredentialMatchesRequest = offeredCredentialsMatchingRequest.find( + (offeredCredential) => + ((offeredCredential.format === 'jwt_vc_json-ld' || offeredCredential.format === 'jwt_vc_json') && + equalsIgnoreOrder(offeredCredential.types, credential.type)) || + (offeredCredential.format === 'ldp_vc' && + equalsIgnoreOrder(offeredCredential.credential_definition.types, credential.type)) + ) + + if (!issuedCredentialMatchesRequest) { + throw new AriesFrameworkError('The credential to be issued does not match the request.') + } + + const sphereonICredential = JsonTransformer.toJSON(credential) as ICredential + + return { + format: credentialRequest.format, + credential: sphereonICredential, + signCallback: this.getCredentialSigningCallback(agentContext, issuerVerificationMethod), + } + } + } + + public async createIssueCredentialResponse(agentContext: AgentContext, options: IssueCredentialOptions) { + const { credentialRequest, credential, verificationMethod } = options + + const issuerMetadata = options.issuerMetadata ?? this.issuerMetadata + const vcIssuer = this.getVcIssuer(agentContext, issuerMetadata) + + const issueCredentialResponse = await vcIssuer.issueCredential({ + credentialRequest, + tokenExpiresIn: this.tokenExpiresIn, + cNonceExpiresIn: this.cNonceExpiresIn, + credentialDataSupplier: this.getCredentialDataSupplier( + agentContext, + credential, + issuerMetadata.credentialsSupported, + verificationMethod + ), + credential: undefined, + newCNonce: undefined, + credentialDataSupplierInput: undefined, + responseCNonce: undefined, + }) + + if (!issueCredentialResponse.credential) { + throw new AriesFrameworkError('No credential defined in the issueCredentialResponse.') + } + + if (issueCredentialResponse.acceptance_token) { + throw new AriesFrameworkError('Acceptance token not yet supported.') + } + + return issueCredentialResponse } } diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts index 0de5ea82f2..b09e228386 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts @@ -1,7 +1,64 @@ -export interface IssueCredentialOptions { - tobedefined: true +import type { VerificationMethod, W3cCredential } from '@aries-framework/core' +import type { + CredentialOfferFormat, + CredentialOfferPayloadV1_0_11, + CredentialRequestV1_0_11, + CredentialSupported, + MetadataDisplay, + ProofOfPossession, +} from '@sphereon/oid4vci-common' + +// If the entry is an object, the object contains the data related to a certain credential type +// the Wallet MAY request. Each object MUST contain a format Claim determining the format +// and further parameters characterizing by the format of the credential to be requested. +export type OfferedCredential = CredentialOfferFormat | string + +export type PreAuthorizedCodeFlowConfig = { + preAuthorizedCode: string + userPinRequired: boolean +} + +export type AuthorizationCodeFlowConfig = { + issuerState: string +} + +export type IssuerMetadata = { + // The Credential Issuer's identifier. (URL using the https scheme) + credentialIssuer: string + credentialEndpoint: string + tokenEndpoint: string + authorizationServer?: string + issuerDisplay?: MetadataDisplay + + credentialsSupported: CredentialSupported[] +} + +export interface CreateCredentialOfferOptions { + // The scheme used for the credentialIssuer. Default is https + scheme?: 'http' | 'https' | 'openid-credential-offer' | string + // The base URI of the credential offer uri + baseUri: string + + preAuthorizedCodeFlowConfig?: PreAuthorizedCodeFlowConfig + authorizationCodeFlowConfig?: AuthorizationCodeFlowConfig + + credentialOfferUri?: string + + issuerMetadata?: IssuerMetadata } -export interface SendCredentialOfferOptions { - tobedefined: true +export type CredentialOfferPayload = CredentialOfferPayloadV1_0_11 + +export type CredentialOfferAndRequest = { + credentialOffer: CredentialOfferPayload + credentialOfferRequest: string +} + +export type CredentialRequest = CredentialRequestV1_0_11 & { proof: ProofOfPossession } + +export interface IssueCredentialOptions { + credentialRequest: CredentialRequest + credential: W3cCredential + verificationMethod: VerificationMethod + issuerMetadata?: IssuerMetadata } diff --git a/packages/openid4vc-issuer/src/index.ts b/packages/openid4vc-issuer/src/index.ts index f74959d07c..30f2c313f6 100644 --- a/packages/openid4vc-issuer/src/index.ts +++ b/packages/openid4vc-issuer/src/index.ts @@ -1,3 +1,5 @@ +export { OpenId4VcIssuerModuleConfig, OpenId4VcIssuerModuleConfigOptions } from './OpenId4VcIssuerModuleConfig' + export * from './OpenId4VcIssuerApi' export * from './OpenId4VcIssuerModule' export * from './OpenId4VcIssuerService' diff --git a/packages/openid4vc-issuer/tests/OpenId4VcIssuerModule.test.ts b/packages/openid4vc-issuer/tests/OpenId4VcIssuerModule.test.ts new file mode 100644 index 0000000000..fb26071112 --- /dev/null +++ b/packages/openid4vc-issuer/tests/OpenId4VcIssuerModule.test.ts @@ -0,0 +1,41 @@ +/* eslint-disable @typescript-eslint/unbound-method */ +import type { DependencyManager } from '@aries-framework/core' + +import { OpenId4VcIssuerApi } from '../src/OpenId4VcIssuerApi' +import { OpenId4VcIssuerModule } from '../src/OpenId4VcIssuerModule' +import { OpenId4VcIssuerModuleConfig } from '../src/OpenId4VcIssuerModuleConfig' +import { OpenId4VcIssuerService } from '../src/OpenId4VcIssuerService' + +const dependencyManager = { + registerInstance: jest.fn(), + registerSingleton: jest.fn(), + registerContextScoped: jest.fn(), + resolve: jest.fn().mockReturnValue({ logger: { warn: jest.fn() } }), +} as unknown as DependencyManager + +describe('OpenId4VcIssuerModule', () => { + test('registers dependencies on the dependency manager', () => { + const issuerMetadata = { + credentialIssuer: 'https://example.com', + credentialEndpoint: 'https://example.com/credentials', + tokenEndpoint: 'https://example.com/token', + credentialsSupported: [], + } + const openId4VcClientModule = new OpenId4VcIssuerModule({ + issuerMetadata, + }) + openId4VcClientModule.register(dependencyManager) + + expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith( + OpenId4VcIssuerModuleConfig, + new OpenId4VcIssuerModuleConfig({ issuerMetadata }) + ) + + expect(dependencyManager.registerContextScoped).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerContextScoped).toHaveBeenCalledWith(OpenId4VcIssuerApi) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VcIssuerService) + }) +}) diff --git a/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts b/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts index aaee28ea80..06c7767a98 100644 --- a/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts +++ b/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts @@ -1,23 +1,176 @@ +import type { + PreAuthorizedCodeFlowConfig, + AuthorizationCodeFlowConfig, + IssuerMetadata, + CredentialRequest, +} from '../src/OpenId4VcIssuerServiceOptions' +import type { + AgentContext, + KeyDidCreateOptions, + VerificationMethod, + W3cVerifiableCredential, + W3cVerifyCredentialResult, +} from '@aries-framework/core' +import type { CredentialSupported } from '@sphereon/oid4vci-common' +import type { OriginalVerifiableCredential as SphereonW3cVerifiableCredential } from '@sphereon/ssi-types' + import { AskarModule } from '@aries-framework/askar' -import { Agent } from '@aries-framework/core' +import { + Agent, + AriesFrameworkError, + DidKey, + DidsApi, + JsonTransformer, + JwsService, + KeyType, + TypedArrayEncoder, + W3cCredential, + W3cCredentialService, + W3cCredentialSubject, + W3cIssuer, + W3cJsonLdVerifiableCredential, + W3cJwtVerifiableCredential, + getJwkFromKey, + getKeyFromVerificationMethod, + w3cDate, +} from '@aries-framework/core' import { agentDependencies } from '@aries-framework/node' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { cleanAll, enableNetConnect } from 'nock' -import { OpenId4VcIssuerModule } from '../src' +import { equalsIgnoreOrder } from '../../core/src/utils/deepEquality' +import { OpenId4VcIssuerModule, OpenId4VcIssuerService } from '../src' + +const openBadgeCredential: CredentialSupported & { id: string } = { + id: 'https://openid4vc-issuer.com/credentials/OpenBadgeCredential', + format: 'jwt_vc_json', + types: ['VerifiableCredential', 'OpenBadgeCredential'], +} + +const universityDegreeCredential: CredentialSupported & { id: string } = { + id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredential', + format: 'jwt_vc_json', + types: ['VerifiableCredential', 'UniversityDegreeCredential'], +} + +const baseCredentialRequestOptions = { + scheme: 'openid-credential-offer', + baseUri: 'openid4vc-issuer.com', +} + +const issuerMetadata: IssuerMetadata = { + credentialIssuer: 'https://openid4vc-issuer.com', + credentialEndpoint: 'https://openid4vc-issuer.com/credentials', + tokenEndpoint: 'https://openid4vc-issuer.com/token', + credentialsSupported: [openBadgeCredential], +} const modules = { - openId4VcIssuer: new OpenId4VcIssuerModule(), - askar: new AskarModule({ - ariesAskar, - }), + openId4VcIssuer: new OpenId4VcIssuerModule({ issuerMetadata }), + askar: new AskarModule({ ariesAskar }), +} + +const jwsService = new JwsService() + +const createCredentialRequestFromKid = async ( + agentContext: AgentContext, + options: { + issuerMetadata: IssuerMetadata + format: 'jwt_vc_json' + types: string[] + nonce: string + kid: string + clientId?: string // use with the authorization code flow, + } +): Promise => { + const { format, types, kid, nonce, issuerMetadata, clientId } = options + + const aud = issuerMetadata.credentialIssuer + + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const didDocument = await didsApi.resolveDidDocument(kid) + if (!didDocument.verificationMethod) { + throw new AriesFrameworkError(`No verification method found for kid ${kid}`) + } + + const verificationMethod = didDocument.dereferenceKey(kid, ['authentication', 'assertionMethod']) + const key = getKeyFromVerificationMethod(verificationMethod) + const jwk = getJwkFromKey(key) + const alg = jwk.supportedSignatureAlgorithms[0] + + const rawPayload = { + iat: Math.floor(Date.now() / 1000), // unix time + iss: clientId, + aud, + nonce, + } + + const payload = TypedArrayEncoder.fromString(JSON.stringify(rawPayload)) + const typ = 'openid4vci-proof+jwt' + + const jws = await jwsService.createJwsCompact(agentContext, { + protectedHeaderOptions: { alg, kid, typ }, + payload, + key, + }) + + // TODO: check different proof types + + return { + format, + types, + proof: { jwt: jws, proof_type: 'jwt' }, + } +} + +async function handleCredentialResponse( + agentContext: AgentContext, + sphereonVerifiableCredential: SphereonW3cVerifiableCredential, + format: string, + types: string[] +) { + const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) + + let result: W3cVerifyCredentialResult + let w3cVerifiableCredential: W3cVerifiableCredential + + if (typeof sphereonVerifiableCredential === 'string') { + if (format !== 'jwt_vc_json' && format !== 'ldp_vc') throw new Error('Invalid format') + // validate json-ld credentials + w3cVerifiableCredential = W3cJwtVerifiableCredential.fromSerializedJwt(sphereonVerifiableCredential) + result = await w3cCredentialService.verifyCredential(agentContext, { credential: w3cVerifiableCredential }) + } else if (format === 'ldp_vc') { + if (format !== 'ldp_vc') throw new Error('Invalid format') + // validate jwt credentials + + w3cVerifiableCredential = JsonTransformer.fromJSON(sphereonVerifiableCredential, W3cJsonLdVerifiableCredential) + result = await w3cCredentialService.verifyCredential(agentContext, { credential: w3cVerifiableCredential }) + } else { + throw new AriesFrameworkError(`Unsupported credential format`) + } + + if (!result.isValid) { + agentContext.config.logger.error('Failed to validate credential', { result }) + throw new AriesFrameworkError(`Failed to validate credential, error = ${result.error?.message ?? 'Unknown'}`) + } + + if (equalsIgnoreOrder(w3cVerifiableCredential.type, types) === false) throw new Error('Invalid credential type') + return w3cVerifiableCredential } describe('OpenId4VcIssuer', () => { - let agent: Agent + let issuer: Agent + let issuerVerificationMethod: VerificationMethod + let issuerDid: string + + let holder: Agent + let holderKid: string + let holderDid: string + + let issuerService: OpenId4VcIssuerService beforeEach(async () => { - agent = new Agent({ + issuer = new Agent({ config: { label: 'OpenId4VcIssuer Test', walletConfig: { @@ -29,22 +182,391 @@ describe('OpenId4VcIssuer', () => { modules, }) - await agent.initialize() + holder = new Agent({ + config: { + label: 'OpenId4VciIssuer(Holder) Test', + walletConfig: { + id: 'openid4vc-Issuer(Holder)-test', + key: 'openid4vc-Issuer(Holder)-test', + }, + }, + dependencies: agentDependencies, + modules, + }) + + await issuer.initialize() + await holder.initialize() + + const holderDidCreateResult = await holder.dids.create({ + method: 'key', + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, + }) + + holderDid = holderDidCreateResult.didState.did as string + const holderDidKey = DidKey.fromDid(holderDidCreateResult.didState.did as string) + holderKid = `${holderDidCreateResult.didState.did as string}#${holderDidKey.key.fingerprint}` + + const issuerDidCreateResult = await issuer.dids.create({ + method: 'key', + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598f') }, + }) + + issuerDid = issuerDidCreateResult.didState.did as string + + const verifierDidKey = DidKey.fromDid(issuerDid) + const verifierKid = `${issuerDidCreateResult.didState.did as string}#${verifierDidKey.key.fingerprint}` + const _issuerVerificationMethod = issuerDidCreateResult.didState.didDocument?.dereferenceKey(verifierKid, [ + 'authentication', + ]) + if (!_issuerVerificationMethod) throw new Error('No verification method found') + issuerVerificationMethod = _issuerVerificationMethod + + issuerService = issuer.context.dependencyManager.resolve(OpenId4VcIssuerService) }) afterEach(async () => { - await agent.shutdown() - await agent.wallet.delete() + await issuer.shutdown() + await issuer.wallet.delete() + + await holder.shutdown() + await holder.wallet.delete() + + cleanAll() + enableNetConnect() }) - describe('[DRAFT 08]: Pre-authorized flow', () => { - afterEach(() => { - cleanAll() - enableNetConnect() + it('pre authorized code flow', async () => { + const cNonce = '1234' + const preAuthorizedCode = '1234567890' + + await issuerService.cNonceStateManager.set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) + + const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, userPinRequired: false } + + const result = await issuer.modules.openId4VcIssuer.createCredentialOffer([openBadgeCredential.id], { + preAuthorizedCodeFlowConfig, + ...baseCredentialRequestOptions, + }) + + expect(result.credentialOfferRequest).toEqual( + 'openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%22%7D' + ) + + const credential = new W3cCredential({ + type: openBadgeCredential.types, + issuer: new W3cIssuer({ id: issuerDid }), + credentialSubject: new W3cCredentialSubject({ id: holderDid }), + issuanceDate: w3cDate(Date.now()), }) - it('test', async () => { - expect(true).toBe(true) + const issueCredentialResponse = await issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ + credential, + verificationMethod: issuerVerificationMethod, + credentialRequest: await createCredentialRequestFromKid(holder.context, { + format: openBadgeCredential.format, + types: openBadgeCredential.types, + issuerMetadata, + kid: holderKid, + nonce: cNonce, + }), }) + + const sphereonW3cCredential = issueCredentialResponse.credential + if (!sphereonW3cCredential) throw new Error('No credential found') + + await handleCredentialResponse( + holder.context, + sphereonW3cCredential, + openBadgeCredential.format, + openBadgeCredential.types + ) + }) + + it('credential id not in credential supported errors', async () => { + const cNonce = '1234' + const preAuthorizedCode = '1234567890' + + await issuerService.cNonceStateManager.set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) + + const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, userPinRequired: false } + + await expect( + issuer.modules.openId4VcIssuer.createCredentialOffer(['invalid id'], { + //issuerMetadata: { + // ...baseIssuerMetadata, + // credentialsSupported: [openBadgeCredential, universityDegreeCredential], + //}, + preAuthorizedCodeFlowConfig, + ...baseCredentialRequestOptions, + }) + ).rejects.toThrowError() + }) + + it('issuing non offered credential errors', async () => { + const cNonce = '1234' + const preAuthorizedCode = '1234567890' + + await issuerService.cNonceStateManager.set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) + + const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, userPinRequired: false } + + const result = await issuer.modules.openId4VcIssuer.createCredentialOffer([openBadgeCredential.id], { + preAuthorizedCodeFlowConfig, + ...baseCredentialRequestOptions, + }) + + expect(result.credentialOfferRequest).toEqual( + 'openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%22%7D' + ) + + const credential = new W3cCredential({ + type: universityDegreeCredential.types, + issuer: new W3cIssuer({ id: issuerDid }), + credentialSubject: new W3cCredentialSubject({ id: holderDid }), + issuanceDate: w3cDate(Date.now()), + }) + + await expect( + issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ + credential, + verificationMethod: issuerVerificationMethod, + credentialRequest: await createCredentialRequestFromKid(holder.context, { + format: openBadgeCredential.format, + types: openBadgeCredential.types, + issuerMetadata, + kid: holderKid, + nonce: cNonce, + }), + }) + ).rejects.toThrowError() + }) + + it('requesting non offered credential errors', async () => { + const cNonce = '1234' + const preAuthorizedCode = '1234567890' + + await issuerService.cNonceStateManager.set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) + + const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { + preAuthorizedCode, + userPinRequired: false, + } + + const result = await issuer.modules.openId4VcIssuer.createCredentialOffer([openBadgeCredential.id], { + preAuthorizedCodeFlowConfig, + ...baseCredentialRequestOptions, + }) + + expect(result.credentialOfferRequest).toEqual( + 'openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%22%7D' + ) + + const credential = new W3cCredential({ + type: openBadgeCredential.types, + issuer: new W3cIssuer({ id: issuerDid }), + credentialSubject: new W3cCredentialSubject({ id: holderDid }), + issuanceDate: w3cDate(Date.now()), + }) + + await expect( + issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ + credential, + verificationMethod: issuerVerificationMethod, + credentialRequest: await createCredentialRequestFromKid(holder.context, { + format: openBadgeCredential.format, + types: universityDegreeCredential.types, + issuerMetadata, + kid: holderKid, + nonce: cNonce, + }), + }) + ).rejects.toThrowError() + }) + + it('authorization code flow', async () => { + const cNonce = '1234' + const issuerState = '1234567890' + + await issuerService.cNonceStateManager.set(cNonce, { cNonce: cNonce, createdAt: Date.now(), issuerState }) + + const authorizationCodeFlowConfig: AuthorizationCodeFlowConfig = { issuerState } + + const result = await issuer.modules.openId4VcIssuer.createCredentialOffer([openBadgeCredential.id], { + authorizationCodeFlowConfig, + ...baseCredentialRequestOptions, + }) + + expect(result.credentialOfferRequest).toEqual( + `openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%221234567890%22%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%22%7D` + ) + + const credential = new W3cCredential({ + type: ['VerifiableCredential', 'OpenBadgeCredential'], + issuer: new W3cIssuer({ id: issuerDid }), + credentialSubject: new W3cCredentialSubject({ id: holderDid }), + issuanceDate: w3cDate(Date.now()), + }) + + const issueCredentialResponse = await issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ + credential, + verificationMethod: issuerVerificationMethod, + credentialRequest: await createCredentialRequestFromKid(holder.context, { + format: openBadgeCredential.format, + types: openBadgeCredential.types, + issuerMetadata, + kid: holderKid, + nonce: cNonce, + clientId: 'required', + }), + }) + + const sphereonW3cCredential = issueCredentialResponse.credential + if (!sphereonW3cCredential) throw new Error('No credential found') + + await handleCredentialResponse( + holder.context, + sphereonW3cCredential, + openBadgeCredential.format, + openBadgeCredential.types + ) + }) + + it('create credential offer and retrieve it from the uri (pre authorized flow)', async () => { + const preAuthorizedCode = '1234567890' + + const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, userPinRequired: false } + + const credentialOfferUri = 'https://openid4vc-issuer.com/credential-offer-uri' + + const { credentialOfferRequest, credentialOffer } = await issuer.modules.openId4VcIssuer.createCredentialOffer( + [openBadgeCredential.id], + { + ...baseCredentialRequestOptions, + credentialOfferUri, + preAuthorizedCodeFlowConfig, + } + ) + + expect(credentialOfferRequest).toEqual( + `openid-credential-offer://openid4vc-issuer.com?credential_offer_uri=${credentialOfferUri}` + ) + + const credentialOfferReceivedByUri = await issuer.modules.openId4VcIssuer.getCredentialOfferFromUri( + credentialOfferUri + ) + + expect(credentialOffer).toEqual(credentialOfferReceivedByUri) + }) + + it('create credential offer and retrieve it from the uri (authorizationCodeFlow)', async () => { + const authorizationCodeFlowConfig: AuthorizationCodeFlowConfig = { issuerState: '1234567890' } + const credentialOfferUri = 'https://openid4vc-issuer.com/credential-offer-uri' + + const { credentialOfferRequest, credentialOffer } = await issuer.modules.openId4VcIssuer.createCredentialOffer( + [openBadgeCredential.id], + { + ...baseCredentialRequestOptions, + credentialOfferUri, + authorizationCodeFlowConfig, + } + ) + + expect(credentialOfferRequest).toEqual( + `openid-credential-offer://openid4vc-issuer.com?credential_offer_uri=${credentialOfferUri}` + ) + + const credentialOfferReceivedByUri = await issuer.modules.openId4VcIssuer.getCredentialOfferFromUri( + credentialOfferUri + ) + + expect(credentialOffer).toEqual(credentialOfferReceivedByUri) + }) + + // https://github.com/orgs/hyperledger/projects/32/views/1?pane=issue&itemId=44709598 + xit('offer and request multiple credentials', async () => { + const cNonce = '1234' + const preAuthorizedCode = '1234567890' + + await issuerService.cNonceStateManager.set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) + + const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, userPinRequired: false } + + const result = await issuer.modules.openId4VcIssuer.createCredentialOffer( + [ + openBadgeCredential.id, + { + format: universityDegreeCredential.format, + types: universityDegreeCredential.types, + }, + ], + { + preAuthorizedCodeFlowConfig, + ...baseCredentialRequestOptions, + } + ) + + expect(result.credentialOfferRequest).toEqual( + 'openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%2C%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%22%7D' + ) + + const credential = new W3cCredential({ + type: openBadgeCredential.types, + issuer: new W3cIssuer({ id: issuerDid }), + credentialSubject: new W3cCredentialSubject({ id: holderDid }), + issuanceDate: w3cDate(Date.now()), + }) + + const issueCredentialResponse = await issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ + credential, + verificationMethod: issuerVerificationMethod, + credentialRequest: await createCredentialRequestFromKid(holder.context, { + format: openBadgeCredential.format, + types: openBadgeCredential.types, + issuerMetadata, + kid: holderKid, + nonce: cNonce, + }), + }) + + const sphereonW3cCredential = issueCredentialResponse.credential + if (!sphereonW3cCredential) throw new Error('No credential found') + + await handleCredentialResponse( + holder.context, + sphereonW3cCredential, + openBadgeCredential.format, + openBadgeCredential.types + ) + + const credential2 = new W3cCredential({ + type: universityDegreeCredential.types, + issuer: new W3cIssuer({ id: issuerDid }), + credentialSubject: new W3cCredentialSubject({ id: holderDid }), + issuanceDate: w3cDate(Date.now()), + }) + + const issueCredentialResponse2 = await issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ + credential: credential2, + verificationMethod: issuerVerificationMethod, + credentialRequest: await createCredentialRequestFromKid(holder.context, { + format: universityDegreeCredential.format, + types: universityDegreeCredential.types, + issuerMetadata, + kid: holderKid, + nonce: cNonce, + }), + }) + + const sphereonW3cCredential2 = issueCredentialResponse2.credential + if (!sphereonW3cCredential2) throw new Error('No credential found') + + await handleCredentialResponse( + holder.context, + sphereonW3cCredential, + universityDegreeCredential.format, + universityDegreeCredential.types + ) }) }) diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts index 47fb7a5575..b02fc35d02 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts @@ -233,7 +233,7 @@ export class OpenId4VcVerifierService { const idTokenPayload = await response.authorizationResponse.idToken.payload() return { - idTokenPayload: idTokenPayload, // TODO: return something else + idTokenPayload: idTokenPayload, submission: presentationDefinition ? response.oid4vpSubmission : undefined, } } diff --git a/packages/openid4vc-verifier/tests/OpenId4VcVerifierModule.test.ts b/packages/openid4vc-verifier/tests/OpenId4VcVerifierModule.test.ts new file mode 100644 index 0000000000..a1cd46065c --- /dev/null +++ b/packages/openid4vc-verifier/tests/OpenId4VcVerifierModule.test.ts @@ -0,0 +1,26 @@ +/* eslint-disable @typescript-eslint/unbound-method */ +import type { DependencyManager } from '@aries-framework/core' + +import { OpenId4VcVerifierApi } from '../src/OpenId4VcVerifierApi' +import { OpenId4VcVerifierModule } from '../src/OpenId4VcVerifierModule' +import { OpenId4VcVerifierService } from '../src/OpenId4VcVerifierService' + +const dependencyManager = { + registerInstance: jest.fn(), + registerSingleton: jest.fn(), + registerContextScoped: jest.fn(), + resolve: jest.fn().mockReturnValue({ logger: { warn: jest.fn() } }), +} as unknown as DependencyManager + +describe('OpenId4VcIssuerModule', () => { + test('registers dependencies on the dependency manager', () => { + const openId4VcClientModule = new OpenId4VcVerifierModule() + openId4VcClientModule.register(dependencyManager) + + expect(dependencyManager.registerContextScoped).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerContextScoped).toHaveBeenCalledWith(OpenId4VcVerifierApi) + + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VcVerifierService) + }) +}) diff --git a/yarn.lock b/yarn.lock index 575ad49909..ecf0a4f389 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2448,15 +2448,6 @@ cross-fetch "^3.1.8" debug "^4.3.4" -"@sphereon/oid4vci-common@0.7.3", "@sphereon/oid4vci-common@^0.7.3": - version "0.7.3" - resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-common/-/oid4vci-common-0.7.3.tgz#188250a0a51c5a5df5424f63781f517788c4b296" - integrity sha512-rfXYXWYsa+wQ22A/IchOJSUzTr6ChuCiZYYvZB44kHEjCdQ15Ix3PKkVD6vmxaoSx7sZ9Q+LQTZbNGvx+7LpWw== - dependencies: - "@sphereon/ssi-types" "0.17.2" - cross-fetch "^3.1.8" - jwt-decode "^3.1.2" - "@sphereon/oid4vci-common@0.8.1", "@sphereon/oid4vci-common@^0.8.1": version "0.8.1" resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-common/-/oid4vci-common-0.8.1.tgz#2623f467c3765a96f3330691d6a0946f04360106" @@ -2466,12 +2457,12 @@ cross-fetch "^3.1.8" jwt-decode "^3.1.2" -"@sphereon/oid4vci-issuer@^0.7.3": - version "0.7.3" - resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-issuer/-/oid4vci-issuer-0.7.3.tgz#862c34b9e4c3c3c95485971cd58471729e4b20db" - integrity sha512-1bS/Z5M5HIU84j7DbUzJ7L+yDGYMG7i8Fm/QEL7QyWfhm9puZcInQwdzcW9VVx6qoNXmFRhBPwcJcNX9C89A/w== +"@sphereon/oid4vci-issuer@^0.8.1": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-issuer/-/oid4vci-issuer-0.8.1.tgz#7682b133982097d84018b4a4d3a8c903b296f214" + integrity sha512-2IK0XdO3djGPeLFmPI5zYcHRfsZG9ZugQ+VyJUNtEO/nfcwXGxd4pCxzaUxqHRCTXEStCxXRnvf4mntYuhnpIQ== dependencies: - "@sphereon/oid4vci-common" "0.7.3" + "@sphereon/oid4vci-common" "0.8.1" "@sphereon/ssi-types" "0.17.2" uuid "^9.0.0" From f1919e2d2b83114c937aeb30acd8465cb3b4b0df Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 15 Nov 2023 15:18:53 +0100 Subject: [PATCH 042/115] fix: verifier remove duplicate Signed-off-by: Martin Auer --- .../src/OpenId4VcVerifierService.ts | 8 ++++-- .../src/utils/signatureAlgorithms.ts | 26 ------------------- 2 files changed, 6 insertions(+), 28 deletions(-) delete mode 100644 packages/openid4vc-verifier/src/utils/signatureAlgorithms.ts diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts index b02fc35d02..1945a0d325 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts @@ -39,8 +39,12 @@ import { } from '@sphereon/did-auth-siop' import { staticOpOpenIdConfig, staticOpSiopConfig } from './OpenId4VcVerifierServiceOptions' -import { getSupportedDidMethods, getSuppliedSignatureFromVerificationMethod, getResolver } from './shared' -import { getSupportedJwaSignatureAlgorithms } from './utils/signatureAlgorithms' +import { + getSupportedDidMethods, + getSuppliedSignatureFromVerificationMethod, + getResolver, + getSupportedJwaSignatureAlgorithms, +} from './shared' /** * @internal diff --git a/packages/openid4vc-verifier/src/utils/signatureAlgorithms.ts b/packages/openid4vc-verifier/src/utils/signatureAlgorithms.ts deleted file mode 100644 index 4c8859396d..0000000000 --- a/packages/openid4vc-verifier/src/utils/signatureAlgorithms.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { AgentContext, JwaSignatureAlgorithm } from '@aries-framework/core' - -import { getJwkClassFromKeyType } from '@aries-framework/core' - -/** - * Returns the JWA Signature Algorithms that are supported by the wallet. - * - * This is an approximation based on the supported key types of the wallet. - * This is not 100% correct as a supporting a key type does not mean you support - * all the algorithms for that key type. However, this needs refactoring of the wallet - * that is planned for the 0.5.0 release. - */ -export function getSupportedJwaSignatureAlgorithms(agentContext: AgentContext): JwaSignatureAlgorithm[] { - const supportedKeyTypes = agentContext.wallet.supportedKeyTypes - - // Extract the supported JWS algs based on the key types the wallet support. - const supportedJwaSignatureAlgorithms = supportedKeyTypes - // Map the supported key types to the supported JWK class - .map(getJwkClassFromKeyType) - // Filter out the undefined values - .filter((jwkClass): jwkClass is Exclude => jwkClass !== undefined) - // Extract the supported JWA signature algorithms from the JWK class - .flatMap((jwkClass) => jwkClass.supportedSignatureAlgorithms) - - return supportedJwaSignatureAlgorithms -} From d36cc27e8d73c77c499c94b5924441725a578df9 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 15 Nov 2023 15:42:03 +0100 Subject: [PATCH 043/115] reactor: verifier miscellaneous changes Signed-off-by: Martin Auer --- .../tests/openid4vp-holder.e2e.test.ts | 18 +++++++++--------- .../src/OpenId4VcVerifierApi.ts | 16 +++++++++------- .../src/OpenId4VcVerifierService.ts | 19 +++++++++---------- .../src/OpenId4VcVerifierServiceOptions.ts | 10 +++++----- packages/openid4vc-verifier/src/index.ts | 2 +- 5 files changed, 33 insertions(+), 32 deletions(-) diff --git a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts index e36a3f3d46..f343038121 100644 --- a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts @@ -147,7 +147,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const createProofRequestOptions: CreateProofRequestOptions = { verificationMethod: verifierVerificationMethod, redirectUri: 'https://acme.com/hello', - holderClientMetadata: staticOpOpenIdConfigEdDSA, + holderMetadata: staticOpOpenIdConfigEdDSA, } //////////////////////////// RP (create request) //////////////////////////// @@ -204,7 +204,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { verificationMethod: verifierVerificationMethod, redirectUri: 'https://acme.com/hello', // TODO: if provided this way client metadata is not resolved for the verification method - issuer: 'https://helloworld.com', + holderIdentifier: 'https://helloworld.com', } //////////////////////////// RP (create request) //////////////////////////// @@ -242,7 +242,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const createProofRequestOptions: CreateProofRequestOptions = { verificationMethod: verifierVerificationMethod, redirectUri: 'https://acme.com/hello', - holderClientMetadata: staticOpOpenIdConfigEdDSA, + holderMetadata: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, } @@ -264,7 +264,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const createProofRequestOptions: CreateProofRequestOptions = { verificationMethod: verifierVerificationMethod, redirectUri: 'https://acme.com/hello', - holderClientMetadata: staticOpOpenIdConfigEdDSA, + holderMetadata: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, } @@ -290,7 +290,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const createProofRequestOptions: CreateProofRequestOptions = { verificationMethod: verifierVerificationMethod, redirectUri: 'https://acme.com/hello', - holderClientMetadata: staticOpOpenIdConfigEdDSA, + holderMetadata: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, } @@ -329,7 +329,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const createProofRequestOptions: CreateProofRequestOptions = { verificationMethod: verifierVerificationMethod, redirectUri: 'https://acme.com/hello', - holderClientMetadata: staticOpOpenIdConfigEdDSA, + holderMetadata: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, } @@ -362,7 +362,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const createProofRequestOptions: CreateProofRequestOptions = { verificationMethod: verifierVerificationMethod, redirectUri: 'https://acme.com/hello', - holderClientMetadata: staticOpOpenIdConfigEdDSA, + holderMetadata: staticOpOpenIdConfigEdDSA, presentationDefinition: combinePresentationDefinitions([ openBadgePresentationDefinition, universityDegreePresentationDefinition, @@ -426,7 +426,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const createProofRequestOptions: CreateProofRequestOptions = { verificationMethod: verifierVerificationMethod, redirectUri: 'https://acme.com/hello', - holderClientMetadata: staticOpOpenIdConfigEdDSA, + holderMetadata: staticOpOpenIdConfigEdDSA, presentationDefinition: combinePresentationDefinitions([ openBadgePresentationDefinition, universityDegreePresentationDefinition, @@ -456,7 +456,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const createProofRequestOptions: CreateProofRequestOptions = { verificationMethod: verifierVerificationMethod, redirectUri: 'https://acme.com/hello', - holderClientMetadata: staticOpOpenIdConfigEdDSA, + holderMetadata: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, } diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts index 3ed3fa577d..09170aed02 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts @@ -23,16 +23,17 @@ export class OpenId4VcVerifierApi { /** * Creates a proof request with the provided options. - * The proof request can be a SIOP request (authentication) or VP request querying verifiable credentials). - * Metadata about the holder can be provided by either passing the @see HolderClientMetadata or by passing the issuer URL. - * If the issuer URL is provided, the metadata will be retrieved from the issuer hosted OpenID configuration. + * The proof request can be a SIOP request (authentication) or VP request (querying verifiable credentials). + * Metadata about the holder can be provided statically @see HolderClientMetadata or dynamically by providing the issuer URL. + * If the issuer URL is provided, the metadata will be retrieved from the issuer hosted OpenID configuration endpoint. * If neither the holder metadata nor the issuer URL is provided, a static configuration defined in @link https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-static-configuration-values * If a presentation definition is provided, a VP request will be created, querying the holder verifiable credentials according to the specifics of the presentation definition. - * @param options.verificationMethod - The method used for verification. + * * @param options.redirect_url - The URL to redirect to after verification. - * @param options.holderClientMetadata - Optional metadata about the holder client. - * @param options.issuer - Optional issuer of the proof request. - * @param options.presentationDefinition - Optional presentation definition for the proof request. + * @param options.holderMetadata - Optional metadata about the holder. + * @param options.holderIdentifier - Optional the identifier of the holder (OpenId-Provider) provider for performing dynamic discovery. + * @param options.presentationDefinition - Optional presentation definition for requesting the presentation of verifiable credentials. + * @param options.verificationMethod - The VerificationMethod to use for signing the proof request. * @returns @see ProofRequestWithMetadata object containing the proof request and metadata for verifying the proof response. */ public async createProofRequest(options: CreateProofRequestOptions) { @@ -44,6 +45,7 @@ export class OpenId4VcVerifierApi { * The proof response validates the idToken, the signature of the received Verifiable Presentation, * as well as that the structure of the Verifiable Presentation matches the provided presentation definition. * + * @param proofPayload - The payload of the proof response. * @param options.createProofRequestOptions - The options used to create the proof request. * @param options.proofRequestMetadata - Metadata about the proof request. * @returns @see VerifiedProofResponse object containing the idTokenPayload and the verified submission. diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts index 1945a0d325..eef369b31c 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts @@ -65,11 +65,11 @@ export class OpenId4VcVerifierService { proofRequestMetadata?: ProofRequestMetadata ) { const { - issuer, + holderIdentifier, redirectUri, presentationDefinition, verificationMethod, - holderClientMetadata: _holderClientMetadata, + holderMetadata: _holderClientMetadata, } = createProofRequestOptions const isVpRequest = presentationDefinition !== undefined @@ -78,11 +78,11 @@ export class OpenId4VcVerifierService { if (_holderClientMetadata) { // use the provided client metadata holderClientMetadata = _holderClientMetadata - } else if (issuer) { + } else if (holderIdentifier) { // Use OpenId Discovery to get the client metadata - let reference_uri = issuer - if (!issuer.endsWith('/.well-known/openid-configuration')) { - reference_uri = issuer + '/.well-known/openid-configuration' + let reference_uri = holderIdentifier + if (!holderIdentifier.endsWith('/.well-known/openid-configuration')) { + reference_uri = holderIdentifier + '/.well-known/openid-configuration' } holderClientMetadata = { reference_uri, passBy: PassBy.REFERENCE, targets: PropertyTarget.REQUEST_OBJECT } } else if (isVpRequest) { @@ -179,7 +179,7 @@ export class OpenId4VcVerifierService { const [noncePart1, noncePart2, state, correlationId] = await generateRandomValues(agentContext, 4) const challenge = noncePart1 + noncePart2 - const relyingParty = await this.getRelyingParty(agentContext, { ...options }) + const relyingParty = await this.getRelyingParty(agentContext, options) const authorizationRequest = await relyingParty.createAuthorizationRequest({ correlationId, @@ -248,8 +248,8 @@ export class OpenId4VcVerifierService { ): PresentationVerificationCallback { const { challenge } = options return async (encodedPresentation, presentationSubmission) => { - this.logger.debug(`Presentation response '${encodedPresentation}'`) - this.logger.debug(`Presentation submission `, presentationSubmission) + this.logger.debug(`Presentation response`, JsonTransformer.toJSON(encodedPresentation)) + this.logger.debug(`Presentation submission`, presentationSubmission) if (!encodedPresentation) { throw new AriesFrameworkError('Did not receive a presentation for verification') @@ -257,7 +257,6 @@ export class OpenId4VcVerifierService { let verificationResult: W3cVerifyPresentationResult if (typeof encodedPresentation === 'string') { - // the presentation is in jwt format (automatically converted to W3cJwtVerifiablePresentation) const presentation = encodedPresentation verificationResult = await this.w3cCredentialService.verifyPresentation(agentContext, { presentation: presentation, diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts index d59f85b431..9e7c94ca4e 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts @@ -9,13 +9,13 @@ import type { IPresentationDefinition } from '@sphereon/pex' import { ResponseType, PassBy, Scope, SigningAlgo, SubjectType } from '@sphereon/did-auth-siop' -export type HolderClientMetadata = ClientMetadataOpts & { authorization_endpoint?: string } +export type HolderMetadata = ClientMetadataOpts & { authorization_endpoint?: string } export interface CreateProofRequestOptions { verificationMethod: VerificationMethod redirectUri: string - holderClientMetadata?: HolderClientMetadata - issuer?: string + holderMetadata?: HolderMetadata + holderIdentifier?: string presentationDefinition?: IPresentationDefinition } @@ -44,7 +44,7 @@ export interface VerifiedProofResponse { export type ProofPayload = AuthorizationResponsePayload -export const staticOpSiopConfig: HolderClientMetadata = { +export const staticOpSiopConfig: HolderMetadata = { authorization_endpoint: 'siopv2:', subject_syntax_types_supported: ['urn:ietf:params:oauth:jwk-thumbprint'], responseTypesSupported: [ResponseType.ID_TOKEN], @@ -55,7 +55,7 @@ export const staticOpSiopConfig: HolderClientMetadata = { passBy: PassBy.VALUE, } -export const staticOpOpenIdConfig: HolderClientMetadata = { +export const staticOpOpenIdConfig: HolderMetadata = { authorization_endpoint: 'openid:', subject_syntax_types_supported: ['urn:ietf:params:oauth:jwk-thumbprint'], responseTypesSupported: [ResponseType.ID_TOKEN, ResponseType.VP_TOKEN], diff --git a/packages/openid4vc-verifier/src/index.ts b/packages/openid4vc-verifier/src/index.ts index 86f27cd6fd..4df091c25e 100644 --- a/packages/openid4vc-verifier/src/index.ts +++ b/packages/openid4vc-verifier/src/index.ts @@ -4,7 +4,7 @@ export * from './OpenId4VcVerifierService' // Contains internal types, so we don't export everything export { - HolderClientMetadata, + HolderMetadata as HolderClientMetadata, staticOpOpenIdConfig, staticOpSiopConfig, CreateProofRequestOptions, From 9459a048ba3c19216898418687fd6bceb3c93e3f Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 15 Nov 2023 16:02:14 +0100 Subject: [PATCH 044/115] reactor: holder miscellaneous changes Signed-off-by: Martin Auer --- .../src/OpenId4VcHolderApi.ts | 19 ++++--- .../src/OpenId4VcHolderServiceOptions.ts | 2 +- .../presentations/OpenId4VpHolderService.ts | 4 +- .../tests/openid4vp-holder.e2e.test.ts | 50 +++++++++---------- 4 files changed, 40 insertions(+), 35 deletions(-) diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts index 74c835a1f2..ae0c248a48 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts @@ -35,19 +35,21 @@ export class OpenId4VcHolderApi { } /** - * Resolves the authentication request given as URI or JWT to a unified @class VerificationRequest, - * then verifies the validity of the request and return the @class VerifiedAuthorizationRequest. + * Resolves the authentication request given as URI or JWT to a unified format, and + * verifies the validity of the request. * The resolved request can be accepted with either @see acceptAuthenticationRequest if it is a * authentication request or with @see acceptPresentationRequest if it is a proofRequest. + * * @param requestJwtOrUri JWT or an openid:// URI - * @returns the resolved and verified Authorization Request + * @returns the resolved and verified authentication request or presentation request alongside the data required to fulfill the presentation request. */ - public async resolveProofRequest(requestOrJwt: string) { - return await this.openId4VpHolderService.resolveProofRequest(this.agentContext, requestOrJwt) + public async resolveProofRequest(requestJwtOrUri: string) { + return await this.openId4VpHolderService.resolveProofRequest(this.agentContext, requestJwtOrUri) } /** * Accepts the authentication request after it has been resolved and verified with @see resolveProofRequest. + * * @param authenticationRequest - The verified authorization request object. * @param verificationMethod - The method used for creating the authentication proof. * @returns @see ProofSubmissionResponse containing the status of the submission. @@ -65,8 +67,10 @@ export class OpenId4VcHolderApi { /** * Accepts the proof request with a presentation after it has been resolved and verified @see resolveProofRequest. + * * @param presentationRequest - The verified authorization request object containing the presentation definition. - * @param presentation - An object containing a presentation submission that fulfills the presentation definition. + * @param presentation.submission - The presentation submission object obtained from @see resolveProofRequest + * @param presentation.submissionEntryIndexes - The indexes of the credentials in the presentation submission that should be send to the verifier. * @returns @see ProofSubmissionResponse containing the status of the submission. */ public async acceptPresentationRequest( @@ -85,7 +89,8 @@ export class OpenId4VcHolderApi { /** * Resolves a credential offer given as payload, credential offer URL, or issuance initiation URL, - * into a unified format.` + * into a unified format. + * * @param credentialOffer the credential offer to resolve * @returns The uniform credential offer payload, the issuer metadata, protocol version, and credentials that can be requested. */ diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts index 6960a54bca..17e8f463a8 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts @@ -14,7 +14,7 @@ export type ResolvedProofRequest = | { proofType: 'presentation' presentationRequest: PresentationRequest - selectResults: PresentationSubmission + presentationSubmission: PresentationSubmission } export type ProofSubmissionResponse = { diff --git a/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts b/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts index 6bb697e9f6..05b56a8af8 100644 --- a/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts +++ b/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts @@ -134,12 +134,12 @@ export class OpenId4VpHolderService { const presentationDefinition = verifiedAuthorizationRequest.presentationDefinitions[0].definition - const selectResults = await this.presentationExchangeService.selectCredentialsForRequest( + const presentationSubmission = await this.presentationExchangeService.selectCredentialsForRequest( agentContext, presentationDefinition ) - return { proofType: 'presentation', presentationRequest: verifiedAuthorizationRequest, selectResults } + return { proofType: 'presentation', presentationRequest: verifiedAuthorizationRequest, presentationSubmission } } /** diff --git a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts index f343038121..8f5c521c59 100644 --- a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts @@ -252,8 +252,8 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const result = await holder.modules.openId4VcHolder.resolveProofRequest(proofRequest) if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') - expect(result.selectResults.areRequirementsSatisfied).toBeFalsy() - expect(result.selectResults.requirements.length).toBe(1) + expect(result.presentationSubmission.areRequirementsSatisfied).toBeFalsy() + expect(result.presentationSubmission.requirements.length).toBe(1) }) it('resolving vp request with wrong credentials errors', async () => { @@ -274,8 +274,8 @@ describe('OpenId4VcHolder | OpenID4VP', () => { if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') //////////////////////////// OP (validate and parse the request) //////////////////////////// - expect(result.selectResults.areRequirementsSatisfied).toBeFalsy() - expect(result.selectResults.requirements.length).toBe(1) + expect(result.presentationSubmission.areRequirementsSatisfied).toBeFalsy() + expect(result.presentationSubmission.requirements.length).toBe(1) }) it('expect submitting a wrong submission to fail', async () => { @@ -311,7 +311,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { await expect( holder.modules.openId4VcHolder.acceptPresentationRequest(resolvedOpenBadge.presentationRequest, { - submission: resolvedUniversityDegree.selectResults, + submission: resolvedUniversityDegree.presentationSubmission, submissionEntryIndexes: [0], }) ).rejects.toThrow() @@ -340,12 +340,12 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const result = await holder.modules.openId4VcHolder.resolveProofRequest(proofRequest) if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') - const { presentationRequest, selectResults } = result - expect(selectResults.areRequirementsSatisfied).toBeTruthy() - expect(selectResults.requirements.length).toBe(1) - expect(selectResults.requirements[0].needsCount).toBe(1) - expect(selectResults.requirements[0].submissionEntry.length).toBe(1) - expect(selectResults.requirements[0].submissionEntry[0].inputDescriptorId).toBe('OpenBadgeCredential') + const { presentationRequest, presentationSubmission } = result + expect(presentationSubmission.areRequirementsSatisfied).toBeTruthy() + expect(presentationSubmission.requirements.length).toBe(1) + expect(presentationSubmission.requirements[0].needsCount).toBe(1) + expect(presentationSubmission.requirements[0].submissionEntry.length).toBe(1) + expect(presentationSubmission.requirements[0].submissionEntry[0].inputDescriptorId).toBe('OpenBadgeCredential') expect(presentationRequest.presentationDefinitions[0].definition).toMatchObject(openBadgePresentationDefinition) }) @@ -378,20 +378,20 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const result = await holder.modules.openId4VcHolder.resolveProofRequest(proofRequest) if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') - const { selectResults } = result - expect(selectResults.areRequirementsSatisfied).toBeTruthy() - expect(selectResults.requirements.length).toBe(2) - expect(selectResults.requirements[0].needsCount).toBe(1) - expect(selectResults.requirements[0].submissionEntry.length).toBe(1) - expect(selectResults.requirements[1].needsCount).toBe(1) - expect(selectResults.requirements[1].submissionEntry.length).toBe(1) - expect(selectResults.requirements[0].submissionEntry[0].inputDescriptorId).toBe('OpenBadgeCredential') - expect(selectResults.requirements[1].submissionEntry[0].inputDescriptorId).toBe('UniversityDegree') + const { presentationSubmission } = result + expect(presentationSubmission.areRequirementsSatisfied).toBeTruthy() + expect(presentationSubmission.requirements.length).toBe(2) + expect(presentationSubmission.requirements[0].needsCount).toBe(1) + expect(presentationSubmission.requirements[0].submissionEntry.length).toBe(1) + expect(presentationSubmission.requirements[1].needsCount).toBe(1) + expect(presentationSubmission.requirements[1].submissionEntry.length).toBe(1) + expect(presentationSubmission.requirements[0].submissionEntry[0].inputDescriptorId).toBe('OpenBadgeCredential') + expect(presentationSubmission.requirements[1].submissionEntry[0].inputDescriptorId).toBe('UniversityDegree') const { submittedResponse } = await holder.modules.openId4VcHolder.acceptPresentationRequest( result.presentationRequest, { - submission: result.selectResults, + submission: result.presentationSubmission, submissionEntryIndexes: [0, 0], } ) @@ -442,7 +442,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { await expect( holder.modules.openId4VcHolder.acceptPresentationRequest(result.presentationRequest, { - submission: result.selectResults, + submission: result.presentationSubmission, submissionEntryIndexes: [0], }) ).rejects.toThrow() @@ -471,9 +471,9 @@ describe('OpenId4VcHolder | OpenID4VP', () => { //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// // Select the appropriate credentials - result.selectResults.requirements[0] + result.presentationSubmission.requirements[0] - if (!result.selectResults.areRequirementsSatisfied) { + if (!result.presentationSubmission.areRequirementsSatisfied) { throw new Error('Requirements are not satisfied.') } @@ -481,7 +481,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const { submittedResponse, status } = await holder.modules.openId4VcHolder.acceptPresentationRequest( result.presentationRequest, { - submission: result.selectResults, + submission: result.presentationSubmission, submissionEntryIndexes: [0], } ) From 8ecc486a40140caf5ccee46e1421dcae8be7f719 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 15 Nov 2023 16:27:05 +0100 Subject: [PATCH 045/115] fix: remove unnecessary dependencies Signed-off-by: Martin Auer --- packages/openid4vc-holder/package.json | 2 -- packages/openid4vc-issuer/package.json | 2 -- packages/openid4vc-verifier/package.json | 2 -- 3 files changed, 6 deletions(-) diff --git a/packages/openid4vc-holder/package.json b/packages/openid4vc-holder/package.json index 9908918a11..ef8474b29d 100644 --- a/packages/openid4vc-holder/package.json +++ b/packages/openid4vc-holder/package.json @@ -34,9 +34,7 @@ "@sphereon/ssi-types": "^0.17.5" }, "devDependencies": { - "@aries-framework/askar": "0.4.2", "@aries-framework/node": "0.4.2", - "@hyperledger/aries-askar-nodejs": "^0.1.0", "@types/jsonpath": "^0.2.1", "nock": "^13.3.0", "rimraf": "^4.4.0", diff --git a/packages/openid4vc-issuer/package.json b/packages/openid4vc-issuer/package.json index bb9787ad78..703f8ab241 100644 --- a/packages/openid4vc-issuer/package.json +++ b/packages/openid4vc-issuer/package.json @@ -30,9 +30,7 @@ "@sphereon/ssi-types": "^0.17.5" }, "devDependencies": { - "@aries-framework/askar": "0.4.2", "@aries-framework/node": "0.4.2", - "@hyperledger/aries-askar-nodejs": "^0.1.0", "nock": "^13.3.0", "rimraf": "^4.4.0", "typescript": "~4.9.5" diff --git a/packages/openid4vc-verifier/package.json b/packages/openid4vc-verifier/package.json index 590c9ef732..0323d7d5ab 100644 --- a/packages/openid4vc-verifier/package.json +++ b/packages/openid4vc-verifier/package.json @@ -29,9 +29,7 @@ "@sphereon/ssi-types": "^0.17.5" }, "devDependencies": { - "@aries-framework/askar": "0.4.2", "@aries-framework/node": "0.4.2", - "@hyperledger/aries-askar-nodejs": "^0.1.0", "nock": "^13.3.0", "rimraf": "^4.4.0", "typescript": "~4.9.5" From fdff7b3b5f018c89970aa237b536dac8179186e2 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 15 Nov 2023 16:45:45 +0100 Subject: [PATCH 046/115] refactor: make type for auth req with code --- packages/openid4vc-holder/src/OpenId4VcHolderApi.ts | 2 +- .../src/OpenId4VcHolderServiceOptions.ts | 4 ++++ .../src/issuance/OpenId4VciHolderService.ts | 10 ++++++---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts index ae0c248a48..3e5ba81e51 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts @@ -154,7 +154,7 @@ export class OpenId4VcHolderApi { ): Promise { return this.openId4VcHolderService.acceptCredentialOffer(this.agentContext, { resolvedCredentialOffer, - resolvedAuthorizationRequest: { ...resolvedAuthorizationRequest, code }, + resolvedAuthorizationRequestWithCode: { ...resolvedAuthorizationRequest, code }, acceptCredentialOfferOptions, }) } diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts index 17e8f463a8..bc7f42ac1d 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts @@ -57,6 +57,10 @@ export interface ResolvedAuthorizationRequest extends AuthCodeFlowOptions { authorizationRequestUri: string } +export interface ResolvedAuthorizationRequestWithCode extends ResolvedAuthorizationRequest { + code: string +} + /** * Options that are used to accept a credential offer for both the pre-authorized code flow and authorization code flow. */ diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts index 58464ef897..b03a1e338b 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts @@ -8,6 +8,7 @@ import type { SupportedCredentialFormats, ResolvedCredentialOffer, ResolvedAuthorizationRequest, + ResolvedAuthorizationRequestWithCode, } from '../OpenId4VcHolderServiceOptions' import type { AgentContext, @@ -368,10 +369,11 @@ export class OpenId4VcHolderService { options: { resolvedCredentialOffer: ResolvedCredentialOffer acceptCredentialOfferOptions: AcceptCredentialOfferOptions - resolvedAuthorizationRequest?: ResolvedAuthorizationRequest & { code: string } + resolvedAuthorizationRequestWithCode?: ResolvedAuthorizationRequestWithCode } ) { - const { resolvedCredentialOffer, acceptCredentialOfferOptions, resolvedAuthorizationRequest } = options + const { resolvedCredentialOffer, acceptCredentialOfferOptions, resolvedAuthorizationRequestWithCode } = options + const { credentialOfferPayload, metadata: _metadata, version } = resolvedCredentialOffer const { credentialsToRequest, userPin, proofOfPossessionVerificationMethodResolver, verifyCredentialStatus } = acceptCredentialOfferOptions @@ -405,8 +407,8 @@ export class OpenId4VcHolderService { let accessTokenResponse: OpenIDResponse const accessTokenClient = new AccessTokenClient() - if (resolvedAuthorizationRequest) { - const { code, codeVerifier, redirectUri } = resolvedAuthorizationRequest + if (resolvedAuthorizationRequestWithCode) { + const { code, codeVerifier, redirectUri } = resolvedAuthorizationRequestWithCode accessTokenResponse = await accessTokenClient.acquireAccessToken({ metadata, credentialOffer: { credential_offer: credentialOfferPayload }, From 0c6d7bb3ef15258c40d07f11d9733612a2ad4f24 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 15 Nov 2023 17:30:25 +0100 Subject: [PATCH 047/115] fix: package.json add askar and afj/node as dependency --- packages/openid4vc-holder/package.json | 4 +++- packages/openid4vc-issuer/package.json | 4 +++- packages/openid4vc-verifier/package.json | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/openid4vc-holder/package.json b/packages/openid4vc-holder/package.json index ef8474b29d..42a85a2b59 100644 --- a/packages/openid4vc-holder/package.json +++ b/packages/openid4vc-holder/package.json @@ -24,6 +24,7 @@ "test": "jest" }, "dependencies": { + "@aries-framework/askar": "^0.4.2", "@aries-framework/core": "0.4.2", "@sphereon/oid4vci-client": "^0.8.1", "@sphereon/oid4vci-common": "^0.8.1", @@ -34,7 +35,8 @@ "@sphereon/ssi-types": "^0.17.5" }, "devDependencies": { - "@aries-framework/node": "0.4.2", + "@aries-framework/node": "^0.4.2", + "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", "@types/jsonpath": "^0.2.1", "nock": "^13.3.0", "rimraf": "^4.4.0", diff --git a/packages/openid4vc-issuer/package.json b/packages/openid4vc-issuer/package.json index 703f8ab241..e5717d585c 100644 --- a/packages/openid4vc-issuer/package.json +++ b/packages/openid4vc-issuer/package.json @@ -24,13 +24,15 @@ "test": "jest" }, "dependencies": { + "@aries-framework/askar": "^0.4.2", "@aries-framework/core": "0.4.2", "@sphereon/oid4vci-issuer": "^0.8.1", "@sphereon/oid4vci-common": "^0.8.1", "@sphereon/ssi-types": "^0.17.5" }, "devDependencies": { - "@aries-framework/node": "0.4.2", + "@aries-framework/node": "^0.4.2", + "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", "nock": "^13.3.0", "rimraf": "^4.4.0", "typescript": "~4.9.5" diff --git a/packages/openid4vc-verifier/package.json b/packages/openid4vc-verifier/package.json index 0323d7d5ab..51d90194cf 100644 --- a/packages/openid4vc-verifier/package.json +++ b/packages/openid4vc-verifier/package.json @@ -24,12 +24,14 @@ "test": "jest" }, "dependencies": { + "@aries-framework/askar": "^0.4.2", "@aries-framework/core": "0.4.2", "@sphereon/did-auth-siop": "^0.5.0-unstable.7", "@sphereon/ssi-types": "^0.17.5" }, "devDependencies": { - "@aries-framework/node": "0.4.2", + "@aries-framework/node": "^0.4.2", + "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", "nock": "^13.3.0", "rimraf": "^4.4.0", "typescript": "~4.9.5" From a7173784a761d5333aaa34acaaaddf2290b1289d Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Thu, 16 Nov 2023 10:38:16 +0100 Subject: [PATCH 048/115] chore: fix universal resolver fallback. name param correctly, and remove duplicate --- .../src/presentations/OpenId4VpHolderService.ts | 6 +++++- packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts | 4 ++-- packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts | 1 - 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts b/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts index 05b56a8af8..ca6d54bc0d 100644 --- a/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts +++ b/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts @@ -107,7 +107,7 @@ export class OpenId4VpHolderService { const verifiedAuthorizationRequest = await openidProvider.verifyAuthorizationRequest(requestJwtOrUri, { verification: { mode: VerificationMode.INTERNAL, - resolveOpts: { resolver: getResolver(agentContext), noUniversalResolverFallback: false }, + resolveOpts: { resolver: getResolver(agentContext), noUniversalResolverFallback: true }, }, }) @@ -160,6 +160,10 @@ export class OpenId4VpHolderService { { signature: suppliedSignature, issuer: verificationMethod.controller, + verification: { + resolveOpts: { resolver: getResolver(agentContext), noUniversalResolverFallback: true }, + mode: VerificationMode.INTERNAL, + }, // https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-aud-of-a-request-object audience: authenticationRequest.authorizationRequestPayload.client_id, } diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts index 09170aed02..95b524dc68 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts @@ -29,9 +29,9 @@ export class OpenId4VcVerifierApi { * If neither the holder metadata nor the issuer URL is provided, a static configuration defined in @link https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-static-configuration-values * If a presentation definition is provided, a VP request will be created, querying the holder verifiable credentials according to the specifics of the presentation definition. * - * @param options.redirect_url - The URL to redirect to after verification. + * @param options.redirectUri - The URL to redirect to after verification. * @param options.holderMetadata - Optional metadata about the holder. - * @param options.holderIdentifier - Optional the identifier of the holder (OpenId-Provider) provider for performing dynamic discovery. + * @param options.holderIdentifier - Optional the identifier of the holder (OpenId-Provider) provider for performing dynamic discovery. How to identifier is obtained is out of scope. * @param options.presentationDefinition - Optional presentation definition for requesting the presentation of verifiable credentials. * @param options.verificationMethod - The VerificationMethod to use for signing the proof request. * @returns @see ProofRequestWithMetadata object containing the proof request and metadata for verifying the proof response. diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts index eef369b31c..d4447c0792 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts @@ -135,7 +135,6 @@ export class OpenId4VcVerifierService { const builder = RP.builder() .withClientId(verificationMethod.id) .withRedirectUri(redirectUri) - .withRequestByValue() .withIssuer(ResponseIss.SELF_ISSUED_V2) .withSuppliedSignature(signature, did, kid, alg) .withSupportedVersions([SupportedVersion.SIOPv2_D11, SupportedVersion.SIOPv2_D12_OID4VP_D18]) From 4c1d3a38b0a77fd1fe7528982f6c97a0501a7479 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Thu, 16 Nov 2023 13:01:23 +0100 Subject: [PATCH 049/115] chore: miscellaneous refactorings --- .../src/OpenId4VcHolderServiceOptions.ts | 2 +- .../src/issuance/OpenId4VciHolderService.ts | 2 +- packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts | 4 ++-- .../openid4vc-issuer/src/OpenId4VcIssuerService.ts | 9 ++++++--- .../src/OpenId4VcIssuerServiceOptions.ts | 9 +++++++-- packages/openid4vc-issuer/src/index.ts | 4 ++-- packages/openid4vc-verifier/src/index.ts | 10 +--------- 7 files changed, 20 insertions(+), 20 deletions(-) diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts index bc7f42ac1d..c4a79d61cf 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts @@ -77,7 +77,7 @@ export interface AcceptCredentialOfferOptions { */ credentialsToRequest?: CredentialToRequest[] - verifyCredentialStatus: boolean + verifyCredentialStatus?: boolean /** * A list of allowed proof of possession signature algorithms in order of preference. diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts index b03a1e338b..ad1ea36cff 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts @@ -515,7 +515,7 @@ export class OpenId4VcHolderService { }) const credential = await this.handleCredentialResponse(agentContext, credentialResponse, { - verifyCredentialStatus, + verifyCredentialStatus: verifyCredentialStatus ?? false, }) // Create credential record, but we don't store it yet (only after the user has accepted the credential) diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts index 212d36c15f..2d8aee71cb 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts @@ -1,5 +1,5 @@ import type { - IssueCredentialOptions, + CreateIssueCredentialResponseOptions, CreateCredentialOfferOptions, CredentialOfferAndRequest, OfferedCredential, @@ -67,7 +67,7 @@ export class OpenId4VcIssuerApi { * @param {string} options.credential - The credential to be issued. * @param {IssuerMetadata} options.issuerMetadata - Metadata about the issuer. */ - public async createIssueCredentialResponse(options: IssueCredentialOptions) { + public async createIssueCredentialResponse(options: CreateIssueCredentialResponseOptions) { return await this.openId4VcIssuerService.createIssueCredentialResponse(this.agentContext, options) } } diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts index b89aa12a0c..e2191465f3 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts @@ -1,10 +1,10 @@ import type { - IssueCredentialOptions, CreateCredentialOfferOptions, AuthorizationCodeFlowConfig, PreAuthorizedCodeFlowConfig, OfferedCredential, IssuerMetadata, + CreateIssueCredentialResponseOptions, } from './OpenId4VcIssuerServiceOptions' import type { AgentContext, @@ -272,7 +272,7 @@ export class OpenId4VcIssuerService { * OPTIONAL. Boolean value specifying whether the Credential Issuer expects presentation of a user PIN along with the Token Request * in a Pre-Authorized Code Flow. Default is false. */ - user_pin_required: preAuthorizedCodeFlowConfig.userPinRequired, + user_pin_required: preAuthorizedCodeFlowConfig.userPinRequired ?? false, }, } } @@ -450,7 +450,10 @@ export class OpenId4VcIssuerService { } } - public async createIssueCredentialResponse(agentContext: AgentContext, options: IssueCredentialOptions) { + public async createIssueCredentialResponse( + agentContext: AgentContext, + options: CreateIssueCredentialResponseOptions + ) { const { credentialRequest, credential, verificationMethod } = options const issuerMetadata = options.issuerMetadata ?? this.issuerMetadata diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts index b09e228386..65e5ffefad 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts @@ -3,11 +3,14 @@ import type { CredentialOfferFormat, CredentialOfferPayloadV1_0_11, CredentialRequestV1_0_11, + CredentialResponse, CredentialSupported, MetadataDisplay, ProofOfPossession, } from '@sphereon/oid4vci-common' +export { CredentialResponse } + // If the entry is an object, the object contains the data related to a certain credential type // the Wallet MAY request. Each object MUST contain a format Claim determining the format // and further parameters characterizing by the format of the credential to be requested. @@ -15,7 +18,7 @@ export type OfferedCredential = CredentialOfferFormat | string export type PreAuthorizedCodeFlowConfig = { preAuthorizedCode: string - userPinRequired: boolean + userPinRequired?: boolean } export type AuthorizationCodeFlowConfig = { @@ -36,7 +39,9 @@ export type IssuerMetadata = { export interface CreateCredentialOfferOptions { // The scheme used for the credentialIssuer. Default is https scheme?: 'http' | 'https' | 'openid-credential-offer' | string + // The base URI of the credential offer uri + // TODO: rename to credentialOfferRequestBaseUri baseUri: string preAuthorizedCodeFlowConfig?: PreAuthorizedCodeFlowConfig @@ -56,7 +61,7 @@ export type CredentialOfferAndRequest = { export type CredentialRequest = CredentialRequestV1_0_11 & { proof: ProofOfPossession } -export interface IssueCredentialOptions { +export interface CreateIssueCredentialResponseOptions { credentialRequest: CredentialRequest credential: W3cCredential verificationMethod: VerificationMethod diff --git a/packages/openid4vc-issuer/src/index.ts b/packages/openid4vc-issuer/src/index.ts index 30f2c313f6..f99285d628 100644 --- a/packages/openid4vc-issuer/src/index.ts +++ b/packages/openid4vc-issuer/src/index.ts @@ -1,5 +1,5 @@ -export { OpenId4VcIssuerModuleConfig, OpenId4VcIssuerModuleConfigOptions } from './OpenId4VcIssuerModuleConfig' - export * from './OpenId4VcIssuerApi' export * from './OpenId4VcIssuerModule' export * from './OpenId4VcIssuerService' +export * from './OpenId4VcIssuerModuleConfig' +export * from './OpenId4VcIssuerServiceOptions' diff --git a/packages/openid4vc-verifier/src/index.ts b/packages/openid4vc-verifier/src/index.ts index 4df091c25e..0eaceb2542 100644 --- a/packages/openid4vc-verifier/src/index.ts +++ b/packages/openid4vc-verifier/src/index.ts @@ -1,12 +1,4 @@ export * from './OpenId4VcVerifierApi' export * from './OpenId4VcVerifierModule' export * from './OpenId4VcVerifierService' - -// Contains internal types, so we don't export everything -export { - HolderMetadata as HolderClientMetadata, - staticOpOpenIdConfig, - staticOpSiopConfig, - CreateProofRequestOptions, - ProofRequestWithMetadata, -} from './OpenId4VcVerifierServiceOptions' +export * from './OpenId4VcVerifierServiceOptions' From 5a3fd6c2494161cbbdb31a7bc865a669a65e163f Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Thu, 16 Nov 2023 17:43:40 +0100 Subject: [PATCH 050/115] fix: export all service options --- packages/openid4vc-holder/src/index.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/openid4vc-holder/src/index.ts b/packages/openid4vc-holder/src/index.ts index 02a938901f..f3c5309203 100644 --- a/packages/openid4vc-holder/src/index.ts +++ b/packages/openid4vc-holder/src/index.ts @@ -1,14 +1,6 @@ export * from './OpenId4VcHolderApi' export * from './issuance/OpenId4VciHolderModule' export * from './issuance/OpenId4VciHolderService' -// Contains internal types, so we don't export everything -export { - AuthCodeFlowOptions, - AcceptCredentialOfferOptions, - ProofOfPossessionVerificationMethodResolver, - ProofOfPossessionVerificationMethodResolverOptions, - RequestCredentialOptions, - SupportedCredentialFormats, -} from './OpenId4VcHolderServiceOptions' +export * from './OpenId4VcHolderServiceOptions' export * from './presentations' export { OpenIdCredentialFormatProfile } from './issuance/utils' From c9a0652dae329553f5619856f28b8f5d02e9f3f5 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Thu, 16 Nov 2023 22:57:06 +0100 Subject: [PATCH 051/115] fix: miscellaneous --- .../presentations/OpenId4VpHolderService.ts | 15 ++++++++++++++ .../src/OpenId4VcVerifierService.ts | 1 + .../src/OpenId4VcVerifierServiceOptions.ts | 20 ++++++++++++------- 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts b/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts index ca6d54bc0d..5aa3f4aad9 100644 --- a/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts +++ b/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts @@ -19,6 +19,7 @@ import { inject, InjectionSymbols, Logger, + parseDid, } from '@aries-framework/core' import { CheckLinkedDomain, @@ -153,6 +154,20 @@ export class OpenId4VpHolderService { ): Promise { const openidProvider = await this.getOpenIdProvider(agentContext, { verificationMethod }) + // TODO: jwk support + const subjectSyntaxTypesSupported = authenticationRequest.registrationMetadataPayload.subject_syntax_types_supported + if (subjectSyntaxTypesSupported) { + const { method } = parseDid(verificationMethod.id) + if (subjectSyntaxTypesSupported.includes(`did:${method}`) === false) { + throw new AriesFrameworkError( + [ + 'The provided verification method is not supported by the issuer.', + `Supported subject syntax types: '${subjectSyntaxTypesSupported.join(', ')}'`, + ].join('\n') + ) + } + } + const suppliedSignature = await getSuppliedSignatureFromVerificationMethod(agentContext, verificationMethod) const authorizationResponseWithCorrelationId = await openidProvider.createAuthorizationResponse( diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts index d4447c0792..c2aee83af8 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts @@ -142,6 +142,7 @@ export class OpenId4VcVerifierService { .withCustomResolver(getResolver(agentContext)) .withResponseMode(ResponseMode.POST) .withResponseType(isVpRequest ? [ResponseType.ID_TOKEN, ResponseType.VP_TOKEN] : ResponseType.ID_TOKEN) + .withScope('openid') .withRequestBy(PassBy.VALUE) .withAuthorizationEndpoint(authorizationEndpoint) .withCheckLinkedDomain(CheckLinkedDomain.NEVER) // check diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts index 9e7c94ca4e..e0eeb32ce2 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts @@ -1,13 +1,19 @@ import type { VerificationMethod } from '@aries-framework/core' -import type { - IDTokenPayload, - VerifiedOpenID4VPSubmission, - ClientMetadataOpts, - AuthorizationResponsePayload, -} from '@sphereon/did-auth-siop' import type { IPresentationDefinition } from '@sphereon/pex' -import { ResponseType, PassBy, Scope, SigningAlgo, SubjectType } from '@sphereon/did-auth-siop' +import { + type IDTokenPayload, + type VerifiedOpenID4VPSubmission, + type ClientMetadataOpts, + type AuthorizationResponsePayload, + ResponseType, + Scope, + PassBy, + SigningAlgo, + SubjectType, +} from '@sphereon/did-auth-siop' + +export { PassBy, SigningAlgo, SubjectType, ResponseType, Scope } from '@sphereon/did-auth-siop' export type HolderMetadata = ClientMetadataOpts & { authorization_endpoint?: string } From ed47cdc4d6ae5f4b0d862c632234ad47f948ec77 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Fri, 17 Nov 2023 11:37:17 +0100 Subject: [PATCH 052/115] refactor: rename exports, change exports --- .../openid4vc-holder/src/OpenId4VcHolderApi.ts | 16 ++++++++-------- ...iHolderModule.ts => OpenId4VcHolderModule.ts} | 11 +++++------ packages/openid4vc-holder/src/index.ts | 4 ++-- .../src/issuance/OpenId4VciHolderService.ts | 2 +- packages/openid4vc-holder/src/issuance/index.ts | 1 + .../tests/OpenId4VcHolderModule.test.ts | 6 +++--- .../tests/openid4vci-holder.e2e.test.ts | 2 +- 7 files changed, 21 insertions(+), 21 deletions(-) rename packages/openid4vc-holder/src/{issuance/OpenId4VciHolderModule.ts => OpenId4VcHolderModule.ts} (76%) create mode 100644 packages/openid4vc-holder/src/issuance/index.ts diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts index 3e5ba81e51..1a726cd307 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts @@ -12,7 +12,7 @@ import type { CredentialOfferPayloadV1_0_11 } from '@sphereon/oid4vci-common' import { injectable, AgentContext } from '@aries-framework/core' -import { OpenId4VcHolderService } from './issuance/OpenId4VciHolderService' +import { OpenId4VciHolderService } from './issuance/OpenId4VciHolderService' import { OpenId4VpHolderService } from './presentations' /** @@ -21,16 +21,16 @@ import { OpenId4VpHolderService } from './presentations' @injectable() export class OpenId4VcHolderApi { private agentContext: AgentContext - private openId4VcHolderService: OpenId4VcHolderService + private openId4VciHolderService: OpenId4VciHolderService private openId4VpHolderService: OpenId4VpHolderService public constructor( agentContext: AgentContext, - openId4VcHolderService: OpenId4VcHolderService, + openId4VcHolderService: OpenId4VciHolderService, openId4VpHolderService: OpenId4VpHolderService ) { this.agentContext = agentContext - this.openId4VcHolderService = openId4VcHolderService + this.openId4VciHolderService = openId4VcHolderService this.openId4VpHolderService = openId4VpHolderService } @@ -95,7 +95,7 @@ export class OpenId4VcHolderApi { * @returns The uniform credential offer payload, the issuer metadata, protocol version, and credentials that can be requested. */ public async resolveCredentialOffer(credentialOffer: string | CredentialOfferPayloadV1_0_11) { - return await this.openId4VcHolderService.resolveCredentialOffer(credentialOffer) + return await this.openId4VciHolderService.resolveCredentialOffer(credentialOffer) } /** @@ -115,7 +115,7 @@ export class OpenId4VcHolderApi { resolvedCredentialOffer: ResolvedCredentialOffer, authCodeFlowOptions: AuthCodeFlowOptions ) { - return await this.openId4VcHolderService.resolveAuthorizationRequest( + return await this.openId4VciHolderService.resolveAuthorizationRequest( this.agentContext, resolvedCredentialOffer, authCodeFlowOptions @@ -132,7 +132,7 @@ export class OpenId4VcHolderApi { resolvedCredentialOffer: ResolvedCredentialOffer, acceptCredentialOfferOptions: AcceptCredentialOfferOptions ): Promise { - return this.openId4VcHolderService.acceptCredentialOffer(this.agentContext, { + return this.openId4VciHolderService.acceptCredentialOffer(this.agentContext, { resolvedCredentialOffer, acceptCredentialOfferOptions, }) @@ -152,7 +152,7 @@ export class OpenId4VcHolderApi { code: string, acceptCredentialOfferOptions: AcceptCredentialOfferOptions ): Promise { - return this.openId4VcHolderService.acceptCredentialOffer(this.agentContext, { + return this.openId4VciHolderService.acceptCredentialOffer(this.agentContext, { resolvedCredentialOffer, resolvedAuthorizationRequestWithCode: { ...resolvedAuthorizationRequest, code }, acceptCredentialOfferOptions, diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderModule.ts b/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts similarity index 76% rename from packages/openid4vc-holder/src/issuance/OpenId4VciHolderModule.ts rename to packages/openid4vc-holder/src/OpenId4VcHolderModule.ts index ee44b81b28..46adb1733d 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderModule.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts @@ -2,11 +2,10 @@ import type { DependencyManager, Module } from '@aries-framework/core' import { AgentConfig } from '@aries-framework/core' -import { OpenId4VcHolderApi } from '../OpenId4VcHolderApi' -import { PresentationExchangeService } from '../presentations' -import { OpenId4VpHolderService } from '../presentations/OpenId4VpHolderService' - -import { OpenId4VcHolderService } from './OpenId4VciHolderService' +import { OpenId4VcHolderApi } from './OpenId4VcHolderApi' +import { OpenId4VciHolderService } from './issuance/OpenId4VciHolderService' +import { PresentationExchangeService } from './presentations' +import { OpenId4VpHolderService } from './presentations/OpenId4VpHolderService' /** * @public @module OpenId4VcHolderModule @@ -30,7 +29,7 @@ export class OpenId4VcHolderModule implements Module { dependencyManager.registerContextScoped(OpenId4VcHolderApi) // Services - dependencyManager.registerSingleton(OpenId4VcHolderService) + dependencyManager.registerSingleton(OpenId4VciHolderService) dependencyManager.registerSingleton(OpenId4VpHolderService) dependencyManager.registerSingleton(PresentationExchangeService) } diff --git a/packages/openid4vc-holder/src/index.ts b/packages/openid4vc-holder/src/index.ts index f3c5309203..a4710c9eb8 100644 --- a/packages/openid4vc-holder/src/index.ts +++ b/packages/openid4vc-holder/src/index.ts @@ -1,6 +1,6 @@ export * from './OpenId4VcHolderApi' -export * from './issuance/OpenId4VciHolderModule' -export * from './issuance/OpenId4VciHolderService' +export * from './OpenId4VcHolderModule' +export * from './issuance' export * from './OpenId4VcHolderServiceOptions' export * from './presentations' export { OpenIdCredentialFormatProfile } from './issuance/utils' diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts index ad1ea36cff..2d61957ce0 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts @@ -187,7 +187,7 @@ async function createAuthorizationRequestUri(options: { } @injectable() -export class OpenId4VcHolderService { +export class OpenId4VciHolderService { private logger: Logger private w3cCredentialService: W3cCredentialService private jwsService: JwsService diff --git a/packages/openid4vc-holder/src/issuance/index.ts b/packages/openid4vc-holder/src/issuance/index.ts new file mode 100644 index 0000000000..4905f3f315 --- /dev/null +++ b/packages/openid4vc-holder/src/issuance/index.ts @@ -0,0 +1 @@ +export * from './OpenId4VciHolderService' diff --git a/packages/openid4vc-holder/tests/OpenId4VcHolderModule.test.ts b/packages/openid4vc-holder/tests/OpenId4VcHolderModule.test.ts index 3540ef8503..8c5dc04491 100644 --- a/packages/openid4vc-holder/tests/OpenId4VcHolderModule.test.ts +++ b/packages/openid4vc-holder/tests/OpenId4VcHolderModule.test.ts @@ -2,8 +2,8 @@ import type { DependencyManager } from '@aries-framework/core' import { OpenId4VcHolderApi } from '../src/OpenId4VcHolderApi' -import { OpenId4VcHolderModule } from '../src/issuance/OpenId4VciHolderModule' -import { OpenId4VcHolderService } from '../src/issuance/OpenId4VciHolderService' +import { OpenId4VcHolderModule } from '../src/OpenId4VcHolderModule' +import { OpenId4VciHolderService } from '../src/issuance/OpenId4VciHolderService' import { OpenId4VpHolderService, PresentationExchangeService } from '../src/presentations' const dependencyManager = { @@ -22,7 +22,7 @@ describe('OpenId4VcHolderModule', () => { expect(dependencyManager.registerContextScoped).toHaveBeenCalledWith(OpenId4VcHolderApi) expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(3) - expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VcHolderService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VciHolderService) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VpHolderService) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(PresentationExchangeService) }) diff --git a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts index 5288b4e278..2fba95c320 100644 --- a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts @@ -15,7 +15,7 @@ import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import nock, { cleanAll, enableNetConnect } from 'nock' import { OpenIdCredentialFormatProfile } from '../src' -import { OpenId4VcHolderModule } from '../src/issuance/OpenId4VciHolderModule' +import { OpenId4VcHolderModule } from '../src/OpenId4VcHolderModule' import { mattrLaunchpadJsonLd_draft_08, From 6e36e10c7badc8a9a454cb717e1b8a85f820d266 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Fri, 17 Nov 2023 12:07:43 +0100 Subject: [PATCH 053/115] refactor: divide vci holder service and vp holder service --- .../src/OpenId4VcHolderApi.ts | 7 ++--- packages/openid4vc-holder/src/index.ts | 2 -- .../src/issuance/OpenId4VciHolderService.ts | 6 ++-- .../OpenId4VciHolderServiceOptions.ts} | 28 ++----------------- .../openid4vc-holder/src/issuance/index.ts | 2 ++ .../src/issuance/utils/index.ts | 1 + .../presentations/OpenId4VpHolderService.ts | 6 ++-- .../OpenId4VpHolderServiceOptions.ts | 21 ++++++++++++++ .../PresentationExchangeService.ts | 2 +- .../src/presentations/index.ts | 6 ++-- 10 files changed, 38 insertions(+), 43 deletions(-) rename packages/openid4vc-holder/src/{OpenId4VcHolderServiceOptions.ts => issuance/OpenId4VciHolderServiceOptions.ts} (88%) create mode 100644 packages/openid4vc-holder/src/presentations/OpenId4VpHolderServiceOptions.ts diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts index 1a726cd307..c08c5b5235 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts @@ -1,12 +1,11 @@ import type { ResolvedCredentialOffer, + ResolvedAuthorizationRequest, AuthCodeFlowOptions, AcceptCredentialOfferOptions, - ResolvedAuthorizationRequest, - AuthenticationRequest, - PresentationRequest, -} from './OpenId4VcHolderServiceOptions' +} from './issuance/OpenId4VciHolderServiceOptions' import type { PresentationSubmission } from './presentations' +import type { AuthenticationRequest, PresentationRequest } from './presentations/OpenId4VpHolderServiceOptions' import type { VerificationMethod, W3cCredentialRecord } from '@aries-framework/core' import type { CredentialOfferPayloadV1_0_11 } from '@sphereon/oid4vci-common' diff --git a/packages/openid4vc-holder/src/index.ts b/packages/openid4vc-holder/src/index.ts index a4710c9eb8..fd71ceb7df 100644 --- a/packages/openid4vc-holder/src/index.ts +++ b/packages/openid4vc-holder/src/index.ts @@ -1,6 +1,4 @@ export * from './OpenId4VcHolderApi' export * from './OpenId4VcHolderModule' export * from './issuance' -export * from './OpenId4VcHolderServiceOptions' export * from './presentations' -export { OpenIdCredentialFormatProfile } from './issuance/utils' diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts index 2d61957ce0..b4948b474d 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts @@ -1,4 +1,3 @@ -import type { OfferedCredentialWithMetadata } from './utils/IssuerMetadataUtils' import type { AuthCodeFlowOptions, CredentialToRequest, @@ -9,7 +8,8 @@ import type { ResolvedCredentialOffer, ResolvedAuthorizationRequest, ResolvedAuthorizationRequestWithCode, -} from '../OpenId4VcHolderServiceOptions' +} from './OpenId4VciHolderServiceOptions' +import type { OfferedCredentialWithMetadata } from './utils/IssuerMetadataUtils' import type { AgentContext, JwaSignatureAlgorithm, @@ -70,9 +70,9 @@ import { JsonURIMode, } from '@sphereon/oid4vci-common' -import { supportedCredentialFormats } from '../OpenId4VcHolderServiceOptions' import { getSupportedJwaSignatureAlgorithms } from '../shared' +import { supportedCredentialFormats } from './OpenId4VciHolderServiceOptions' import { OpenIdCredentialFormatProfile, fromOpenIdCredentialFormatProfileToDifClaimFormat } from './utils' import { getFormatForVersion, getUniformFormat } from './utils/Formats' import { diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderServiceOptions.ts similarity index 88% rename from packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts rename to packages/openid4vc-holder/src/issuance/OpenId4VciHolderServiceOptions.ts index c4a79d61cf..f4fe7599b1 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderServiceOptions.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderServiceOptions.ts @@ -1,33 +1,9 @@ -import type { OfferedCredentialType } from './issuance/utils/IssuerMetadataUtils' -import type { PresentationSubmission, VerifiedAuthorizationRequestWithPresentationDefinition } from './presentations' +import type { OfferedCredentialType } from './utils/IssuerMetadataUtils' import type { JwaSignatureAlgorithm, KeyType, VerificationMethod } from '@aries-framework/core' -import type { AuthorizationResponsePayload, VerifiedAuthorizationRequest } from '@sphereon/did-auth-siop' import type { CredentialOfferPayloadV1_0_11, EndpointMetadataResult, OpenId4VCIVersion } from '@sphereon/oid4vci-common' -import { OpenIdCredentialFormatProfile } from './issuance/utils/claimFormatMapping' +import { OpenIdCredentialFormatProfile } from './utils/claimFormatMapping' -export type AuthenticationRequest = VerifiedAuthorizationRequest -export type PresentationRequest = VerifiedAuthorizationRequestWithPresentationDefinition - -export type ResolvedProofRequest = - | { proofType: 'authentication'; authenticationRequest: AuthenticationRequest } - | { - proofType: 'presentation' - presentationRequest: PresentationRequest - presentationSubmission: PresentationSubmission - } - -export type ProofSubmissionResponse = { - ok: boolean - status: number - submittedResponse: AuthorizationResponsePayload -} - -export type VpFormat = 'jwt_vp' | 'ldp_vp' - -/** - * The credential formats that are supported by the openid4vc holder - */ export type SupportedCredentialFormats = OpenIdCredentialFormatProfile.JwtVcJson | OpenIdCredentialFormatProfile.LdpVc export const supportedCredentialFormats = [ diff --git a/packages/openid4vc-holder/src/issuance/index.ts b/packages/openid4vc-holder/src/issuance/index.ts index 4905f3f315..125586e082 100644 --- a/packages/openid4vc-holder/src/issuance/index.ts +++ b/packages/openid4vc-holder/src/issuance/index.ts @@ -1 +1,3 @@ export * from './OpenId4VciHolderService' +export * from './OpenId4VciHolderServiceOptions' +export * from './utils' diff --git a/packages/openid4vc-holder/src/issuance/utils/index.ts b/packages/openid4vc-holder/src/issuance/utils/index.ts index 425dcebc12..761d341d06 100644 --- a/packages/openid4vc-holder/src/issuance/utils/index.ts +++ b/packages/openid4vc-holder/src/issuance/utils/index.ts @@ -1 +1,2 @@ export * from './claimFormatMapping' +export * from './Formats' diff --git a/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts b/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts index 5aa3f4aad9..3d131d3d54 100644 --- a/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts +++ b/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts @@ -1,11 +1,11 @@ -import type { PresentationSubmission } from './selection' -import type { InputDescriptorToCredentials } from './selection/types' import type { AuthenticationRequest, PresentationRequest, ProofSubmissionResponse, ResolvedProofRequest, -} from '../OpenId4VcHolderServiceOptions' +} from './OpenId4VpHolderServiceOptions' +import type { PresentationSubmission } from './selection' +import type { InputDescriptorToCredentials } from './selection/types' import type { AgentContext, VerificationMethod, W3cVerifiablePresentation } from '@aries-framework/core' import type { PresentationDefinitionWithLocation, VerifiedAuthorizationRequest } from '@sphereon/did-auth-siop' import type { W3CVerifiablePresentation } from '@sphereon/ssi-types' diff --git a/packages/openid4vc-holder/src/presentations/OpenId4VpHolderServiceOptions.ts b/packages/openid4vc-holder/src/presentations/OpenId4VpHolderServiceOptions.ts new file mode 100644 index 0000000000..7a0252b219 --- /dev/null +++ b/packages/openid4vc-holder/src/presentations/OpenId4VpHolderServiceOptions.ts @@ -0,0 +1,21 @@ +import type { PresentationSubmission, VerifiedAuthorizationRequestWithPresentationDefinition } from '..' +import type { AuthorizationResponsePayload, VerifiedAuthorizationRequest } from '@sphereon/did-auth-siop' + +export type AuthenticationRequest = VerifiedAuthorizationRequest +export type PresentationRequest = VerifiedAuthorizationRequestWithPresentationDefinition + +export type ResolvedProofRequest = + | { proofType: 'authentication'; authenticationRequest: AuthenticationRequest } + | { + proofType: 'presentation' + presentationRequest: PresentationRequest + presentationSubmission: PresentationSubmission + } + +export type ProofSubmissionResponse = { + ok: boolean + status: number + submittedResponse: AuthorizationResponsePayload +} + +export type VpFormat = 'jwt_vp' | 'ldp_vp' diff --git a/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts b/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts index 55e99c4fe6..4d178a08a2 100644 --- a/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts +++ b/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts @@ -1,5 +1,5 @@ +import type { VpFormat } from './OpenId4VpHolderServiceOptions' import type { InputDescriptorToCredentials, PresentationSubmission } from './selection/types' -import type { VpFormat } from '../OpenId4VcHolderServiceOptions' import type { AgentContext, Query, diff --git a/packages/openid4vc-holder/src/presentations/index.ts b/packages/openid4vc-holder/src/presentations/index.ts index 7624253db7..b2fa029010 100644 --- a/packages/openid4vc-holder/src/presentations/index.ts +++ b/packages/openid4vc-holder/src/presentations/index.ts @@ -1,6 +1,4 @@ -export { - OpenId4VpHolderService, - VerifiedAuthorizationRequestWithPresentationDefinition, -} from './OpenId4VpHolderService' +export * from './OpenId4VpHolderService' +export * from './OpenId4VpHolderServiceOptions' export { PresentationExchangeService } from './PresentationExchangeService' export { PresentationSubmission, SubmissionEntry } from './selection' From 648d59e97dfe93ea67f26951abfbbe12f86436e6 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Fri, 17 Nov 2023 12:50:00 +0100 Subject: [PATCH 054/115] fix: package.json's and export presentation definition --- packages/openid4vc-holder/package.json | 6 +++--- packages/openid4vc-verifier/package.json | 2 +- .../src/OpenId4VcVerifierServiceOptions.ts | 6 ++++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/openid4vc-holder/package.json b/packages/openid4vc-holder/package.json index 42a85a2b59..512ba2ec81 100644 --- a/packages/openid4vc-holder/package.json +++ b/packages/openid4vc-holder/package.json @@ -26,13 +26,13 @@ "dependencies": { "@aries-framework/askar": "^0.4.2", "@aries-framework/core": "0.4.2", + "@sphereon/did-auth-siop": "^0.5.0-unstable.7", "@sphereon/oid4vci-client": "^0.8.1", "@sphereon/oid4vci-common": "^0.8.1", - "@sphereon/did-auth-siop": "^0.5.0-unstable.7", "@sphereon/pex": "2.2.0", "@sphereon/pex-models": "^2.1.1", - "jsonpath": "1.1.1", - "@sphereon/ssi-types": "^0.17.5" + "@sphereon/ssi-types": "^0.17.5", + "jsonpath": "1.1.1" }, "devDependencies": { "@aries-framework/node": "^0.4.2", diff --git a/packages/openid4vc-verifier/package.json b/packages/openid4vc-verifier/package.json index 51d90194cf..f1be2f7ebf 100644 --- a/packages/openid4vc-verifier/package.json +++ b/packages/openid4vc-verifier/package.json @@ -27,7 +27,7 @@ "@aries-framework/askar": "^0.4.2", "@aries-framework/core": "0.4.2", "@sphereon/did-auth-siop": "^0.5.0-unstable.7", - "@sphereon/ssi-types": "^0.17.5" + "@sphereon/pex-models": "^2.1.1" }, "devDependencies": { "@aries-framework/node": "^0.4.2", diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts index e0eeb32ce2..06215ca594 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts @@ -1,5 +1,5 @@ import type { VerificationMethod } from '@aries-framework/core' -import type { IPresentationDefinition } from '@sphereon/pex' +import type { PresentationDefinitionV1, PresentationDefinitionV2 } from '@sphereon/pex-models' import { type IDTokenPayload, @@ -17,12 +17,14 @@ export { PassBy, SigningAlgo, SubjectType, ResponseType, Scope } from '@sphereon export type HolderMetadata = ClientMetadataOpts & { authorization_endpoint?: string } +export { PresentationDefinitionV1, PresentationDefinitionV2 } from '@sphereon/pex-models' + export interface CreateProofRequestOptions { verificationMethod: VerificationMethod redirectUri: string holderMetadata?: HolderMetadata holderIdentifier?: string - presentationDefinition?: IPresentationDefinition + presentationDefinition?: PresentationDefinitionV1 | PresentationDefinitionV2 } export type ProofRequest = string From 5aed9626158b645ec4c4501c90f9e9e2c00e7fea Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 20 Nov 2023 08:49:07 +0100 Subject: [PATCH 055/115] fix: don't export authdetails --- .../openid4vc-holder/src/issuance/OpenId4VciHolderService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts index b4948b474d..bb902e471d 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts @@ -82,7 +82,7 @@ import { OfferedCredentialType, } from './utils/IssuerMetadataUtils' -export interface AuthDetails { +interface AuthDetails { type: 'openid_credential' | string locations?: string | string[] format: CredentialFormat | CredentialFormat[] From 668e24c25ffd0ac91fe4ca8c364d0a46991babde Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 20 Nov 2023 09:17:14 +0100 Subject: [PATCH 056/115] refactor: clean up some things --- .../openid4vc-verifier/src/OpenId4VcVerifierService.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts index c2aee83af8..4ddbf4e1dc 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts @@ -145,7 +145,7 @@ export class OpenId4VcVerifierService { .withScope('openid') .withRequestBy(PassBy.VALUE) .withAuthorizationEndpoint(authorizationEndpoint) - .withCheckLinkedDomain(CheckLinkedDomain.NEVER) // check + .withCheckLinkedDomain(CheckLinkedDomain.NEVER) .withRevocationVerification(RevocationVerification.NEVER) // .withWellknownDIDVerifyCallback // .withEventEmitter @@ -153,11 +153,11 @@ export class OpenId4VcVerifierService { if (proofRequestMetadata) { builder.withPresentationVerification( - this.handlePresentationResponse(agentContext, { challenge: proofRequestMetadata.challenge }) + this.getPresentationVerificationCallback(agentContext, { challenge: proofRequestMetadata.challenge }) ) } - if (presentationDefinition) { + if (isVpRequest) { builder.withPresentationDefinition({ definition: presentationDefinition }, [ PropertyTarget.REQUEST_OBJECT, PropertyTarget.AUTHORIZATION_REQUEST, @@ -242,7 +242,7 @@ export class OpenId4VcVerifierService { } } - private handlePresentationResponse( + private getPresentationVerificationCallback( agentContext: AgentContext, options: { challenge: string } ): PresentationVerificationCallback { From bfb17202c6c35a806ed13570e00178b5df672f35 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 20 Nov 2023 09:18:58 +0100 Subject: [PATCH 057/115] refactor: rename presentations folder to presentation --- packages/openid4vc-holder/src/OpenId4VcHolderApi.ts | 6 +++--- packages/openid4vc-holder/src/OpenId4VcHolderModule.ts | 4 ++-- packages/openid4vc-holder/src/index.ts | 2 +- .../OpenId4VpHolderService.ts | 0 .../OpenId4VpHolderServiceOptions.ts | 0 .../PresentationExchangeService.ts | 0 .../src/{presentations => presentation}/index.ts | 0 .../selection/PexCredentialSelection.ts | 0 .../{presentations => presentation}/selection/example.md | 0 .../src/{presentations => presentation}/selection/index.ts | 0 .../src/{presentations => presentation}/selection/types.ts | 0 .../src/{presentations => presentation}/transform.ts | 0 .../openid4vc-holder/tests/OpenId4VcHolderModule.test.ts | 2 +- 13 files changed, 7 insertions(+), 7 deletions(-) rename packages/openid4vc-holder/src/{presentations => presentation}/OpenId4VpHolderService.ts (100%) rename packages/openid4vc-holder/src/{presentations => presentation}/OpenId4VpHolderServiceOptions.ts (100%) rename packages/openid4vc-holder/src/{presentations => presentation}/PresentationExchangeService.ts (100%) rename packages/openid4vc-holder/src/{presentations => presentation}/index.ts (100%) rename packages/openid4vc-holder/src/{presentations => presentation}/selection/PexCredentialSelection.ts (100%) rename packages/openid4vc-holder/src/{presentations => presentation}/selection/example.md (100%) rename packages/openid4vc-holder/src/{presentations => presentation}/selection/index.ts (100%) rename packages/openid4vc-holder/src/{presentations => presentation}/selection/types.ts (100%) rename packages/openid4vc-holder/src/{presentations => presentation}/transform.ts (100%) diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts index c08c5b5235..1afe5cb3d4 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts @@ -4,15 +4,15 @@ import type { AuthCodeFlowOptions, AcceptCredentialOfferOptions, } from './issuance/OpenId4VciHolderServiceOptions' -import type { PresentationSubmission } from './presentations' -import type { AuthenticationRequest, PresentationRequest } from './presentations/OpenId4VpHolderServiceOptions' +import type { PresentationSubmission } from './presentation' +import type { AuthenticationRequest, PresentationRequest } from './presentation/OpenId4VpHolderServiceOptions' import type { VerificationMethod, W3cCredentialRecord } from '@aries-framework/core' import type { CredentialOfferPayloadV1_0_11 } from '@sphereon/oid4vci-common' import { injectable, AgentContext } from '@aries-framework/core' import { OpenId4VciHolderService } from './issuance/OpenId4VciHolderService' -import { OpenId4VpHolderService } from './presentations' +import { OpenId4VpHolderService } from './presentation' /** * @public diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts b/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts index 46adb1733d..075a919280 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts @@ -4,8 +4,8 @@ import { AgentConfig } from '@aries-framework/core' import { OpenId4VcHolderApi } from './OpenId4VcHolderApi' import { OpenId4VciHolderService } from './issuance/OpenId4VciHolderService' -import { PresentationExchangeService } from './presentations' -import { OpenId4VpHolderService } from './presentations/OpenId4VpHolderService' +import { PresentationExchangeService } from './presentation' +import { OpenId4VpHolderService } from './presentation/OpenId4VpHolderService' /** * @public @module OpenId4VcHolderModule diff --git a/packages/openid4vc-holder/src/index.ts b/packages/openid4vc-holder/src/index.ts index fd71ceb7df..ef6371a392 100644 --- a/packages/openid4vc-holder/src/index.ts +++ b/packages/openid4vc-holder/src/index.ts @@ -1,4 +1,4 @@ export * from './OpenId4VcHolderApi' export * from './OpenId4VcHolderModule' export * from './issuance' -export * from './presentations' +export * from './presentation' diff --git a/packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts b/packages/openid4vc-holder/src/presentation/OpenId4VpHolderService.ts similarity index 100% rename from packages/openid4vc-holder/src/presentations/OpenId4VpHolderService.ts rename to packages/openid4vc-holder/src/presentation/OpenId4VpHolderService.ts diff --git a/packages/openid4vc-holder/src/presentations/OpenId4VpHolderServiceOptions.ts b/packages/openid4vc-holder/src/presentation/OpenId4VpHolderServiceOptions.ts similarity index 100% rename from packages/openid4vc-holder/src/presentations/OpenId4VpHolderServiceOptions.ts rename to packages/openid4vc-holder/src/presentation/OpenId4VpHolderServiceOptions.ts diff --git a/packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts b/packages/openid4vc-holder/src/presentation/PresentationExchangeService.ts similarity index 100% rename from packages/openid4vc-holder/src/presentations/PresentationExchangeService.ts rename to packages/openid4vc-holder/src/presentation/PresentationExchangeService.ts diff --git a/packages/openid4vc-holder/src/presentations/index.ts b/packages/openid4vc-holder/src/presentation/index.ts similarity index 100% rename from packages/openid4vc-holder/src/presentations/index.ts rename to packages/openid4vc-holder/src/presentation/index.ts diff --git a/packages/openid4vc-holder/src/presentations/selection/PexCredentialSelection.ts b/packages/openid4vc-holder/src/presentation/selection/PexCredentialSelection.ts similarity index 100% rename from packages/openid4vc-holder/src/presentations/selection/PexCredentialSelection.ts rename to packages/openid4vc-holder/src/presentation/selection/PexCredentialSelection.ts diff --git a/packages/openid4vc-holder/src/presentations/selection/example.md b/packages/openid4vc-holder/src/presentation/selection/example.md similarity index 100% rename from packages/openid4vc-holder/src/presentations/selection/example.md rename to packages/openid4vc-holder/src/presentation/selection/example.md diff --git a/packages/openid4vc-holder/src/presentations/selection/index.ts b/packages/openid4vc-holder/src/presentation/selection/index.ts similarity index 100% rename from packages/openid4vc-holder/src/presentations/selection/index.ts rename to packages/openid4vc-holder/src/presentation/selection/index.ts diff --git a/packages/openid4vc-holder/src/presentations/selection/types.ts b/packages/openid4vc-holder/src/presentation/selection/types.ts similarity index 100% rename from packages/openid4vc-holder/src/presentations/selection/types.ts rename to packages/openid4vc-holder/src/presentation/selection/types.ts diff --git a/packages/openid4vc-holder/src/presentations/transform.ts b/packages/openid4vc-holder/src/presentation/transform.ts similarity index 100% rename from packages/openid4vc-holder/src/presentations/transform.ts rename to packages/openid4vc-holder/src/presentation/transform.ts diff --git a/packages/openid4vc-holder/tests/OpenId4VcHolderModule.test.ts b/packages/openid4vc-holder/tests/OpenId4VcHolderModule.test.ts index 8c5dc04491..8ea31c0ed0 100644 --- a/packages/openid4vc-holder/tests/OpenId4VcHolderModule.test.ts +++ b/packages/openid4vc-holder/tests/OpenId4VcHolderModule.test.ts @@ -4,7 +4,7 @@ import type { DependencyManager } from '@aries-framework/core' import { OpenId4VcHolderApi } from '../src/OpenId4VcHolderApi' import { OpenId4VcHolderModule } from '../src/OpenId4VcHolderModule' import { OpenId4VciHolderService } from '../src/issuance/OpenId4VciHolderService' -import { OpenId4VpHolderService, PresentationExchangeService } from '../src/presentations' +import { OpenId4VpHolderService, PresentationExchangeService } from '../src/presentation' const dependencyManager = { registerInstance: jest.fn(), From e1b6257f47994ee8ec23ff76d4aa05516d067f06 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 20 Nov 2023 11:43:41 +0100 Subject: [PATCH 058/115] fix: authorization endpoint --- packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts index 4ddbf4e1dc..3bff2cdbb9 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts @@ -129,7 +129,7 @@ export class OpenId4VcVerifierService { } } - const authorizationEndpoint = holderClientMetadata.authorization_endpoint ?? isVpRequest ? 'openid:' : 'siopv2:' + const authorizationEndpoint = holderClientMetadata.authorization_endpoint ?? (isVpRequest ? 'openid:' : 'siopv2:') // Check: audience must be set to the issuer with dynamic disc otherwise self-issed.me/v2. const builder = RP.builder() From 2bf9e4fab063ee23a73b92243a5af929c173f3c7 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 20 Nov 2023 11:47:26 +0100 Subject: [PATCH 059/115] fix: AuthDetails not exported --- .../openid4vc-holder/src/issuance/OpenId4VciHolderService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts index bb902e471d..b4948b474d 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts @@ -82,7 +82,7 @@ import { OfferedCredentialType, } from './utils/IssuerMetadataUtils' -interface AuthDetails { +export interface AuthDetails { type: 'openid_credential' | string locations?: string | string[] format: CredentialFormat | CredentialFormat[] From 1be3576f8ea80b4e614f2bfe96d0c4c52653ccbd Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 20 Nov 2023 11:53:11 +0100 Subject: [PATCH 060/115] refactor: module test names --- ...nId4VcHolderModule.test.ts => openId4vc-holder-module.test.ts} | 0 ...nId4VcIssuerModule.test.ts => openId4vc-issuer-module.test.ts} | 0 ...VcVerifierModule.test.ts => openId4vc-verifier-module.test.ts} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename packages/openid4vc-holder/tests/{OpenId4VcHolderModule.test.ts => openId4vc-holder-module.test.ts} (100%) rename packages/openid4vc-issuer/tests/{OpenId4VcIssuerModule.test.ts => openId4vc-issuer-module.test.ts} (100%) rename packages/openid4vc-verifier/tests/{OpenId4VcVerifierModule.test.ts => openId4vc-verifier-module.test.ts} (100%) diff --git a/packages/openid4vc-holder/tests/OpenId4VcHolderModule.test.ts b/packages/openid4vc-holder/tests/openId4vc-holder-module.test.ts similarity index 100% rename from packages/openid4vc-holder/tests/OpenId4VcHolderModule.test.ts rename to packages/openid4vc-holder/tests/openId4vc-holder-module.test.ts diff --git a/packages/openid4vc-issuer/tests/OpenId4VcIssuerModule.test.ts b/packages/openid4vc-issuer/tests/openId4vc-issuer-module.test.ts similarity index 100% rename from packages/openid4vc-issuer/tests/OpenId4VcIssuerModule.test.ts rename to packages/openid4vc-issuer/tests/openId4vc-issuer-module.test.ts diff --git a/packages/openid4vc-verifier/tests/OpenId4VcVerifierModule.test.ts b/packages/openid4vc-verifier/tests/openId4vc-verifier-module.test.ts similarity index 100% rename from packages/openid4vc-verifier/tests/OpenId4VcVerifierModule.test.ts rename to packages/openid4vc-verifier/tests/openId4vc-verifier-module.test.ts From 00a517d6c181c08cbc7f204463ad80706a60ef02 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 20 Nov 2023 11:58:32 +0100 Subject: [PATCH 061/115] test: add some tests for the verifier proof request --- .../tests/openid4vc-verifier.e2e.test.ts | 123 +++++++++++++++++- 1 file changed, 118 insertions(+), 5 deletions(-) diff --git a/packages/openid4vc-verifier/tests/openid4vc-verifier.e2e.test.ts b/packages/openid4vc-verifier/tests/openid4vc-verifier.e2e.test.ts index 741d6f031a..d3f45bdaae 100644 --- a/packages/openid4vc-verifier/tests/openid4vc-verifier.e2e.test.ts +++ b/packages/openid4vc-verifier/tests/openid4vc-verifier.e2e.test.ts @@ -1,10 +1,14 @@ +import type { HolderMetadata, PresentationDefinitionV2 } from '../src' +import type { KeyDidCreateOptions, VerificationMethod } from '@aries-framework/core' + import { AskarModule } from '@aries-framework/askar' -import { Agent } from '@aries-framework/core' +import { Agent, DidKey, Jwt, KeyType, TypedArrayEncoder } from '@aries-framework/core' import { agentDependencies } from '@aries-framework/node' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' +import { SigningAlgo } from '@sphereon/did-auth-siop' import { cleanAll, enableNetConnect } from 'nock' -import { OpenId4VcVerifierModule } from '../src' +import { OpenId4VcVerifierModule, staticOpOpenIdConfig, staticOpSiopConfig } from '../src' const modules = { openId4VcVerifier: new OpenId4VcVerifierModule(), @@ -13,8 +17,40 @@ const modules = { }), } +export const staticSiopConfigEDDSA: HolderMetadata = { + ...staticOpSiopConfig, + idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA], + requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA], + vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] }, jwt_vp: { alg: [SigningAlgo.EDDSA] } }, +} + +export const staticOpOpenIdConfigEDDSA: HolderMetadata = { + ...staticOpOpenIdConfig, + idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA], + requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA], + vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] }, jwt_vp: { alg: [SigningAlgo.EDDSA] } }, +} + +const universityDegreePresentationDefinition: PresentationDefinitionV2 = { + id: 'UniversityDegreeCredential', + input_descriptors: [ + { + id: 'UniversityDegree', + // changed jwt_vc_json to jwt_vc + format: { jwt_vc: { alg: ['EdDSA'] } }, + // changed $.type to $.vc.type + constraints: { + fields: [{ path: ['$.vc.type.*'], filter: { type: 'string', pattern: 'UniversityDegree' } }], + }, + }, + ], +} + describe('OpenId4VcVerifier', () => { let agent: Agent + let did: string + let kid: string + let verificationMethod: VerificationMethod beforeEach(async () => { agent = new Agent({ @@ -30,6 +66,20 @@ describe('OpenId4VcVerifier', () => { }) await agent.initialize() + + const _did = await agent.dids.create({ + method: 'key', + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598f') }, + }) + + did = _did.didState.did as string + + const didKey = DidKey.fromDid(did) + kid = `${did}#${didKey.key.fingerprint}` + const _verificationMethod = _did.didState.didDocument?.dereferenceKey(kid, ['authentication']) + if (!_verificationMethod) throw new Error('No verification method found') + verificationMethod = _verificationMethod }) afterEach(async () => { @@ -37,14 +87,77 @@ describe('OpenId4VcVerifier', () => { await agent.wallet.delete() }) - describe('[DRAFT 08]: Pre-authorized flow', () => { + describe('Verification', () => { afterEach(() => { cleanAll() enableNetConnect() }) - it('test', async () => { - expect(true).toBe(true) + it(`cannot sign authorization request with alg that isn't supported by the OpenId Provider`, async () => { + await expect( + agent.modules.openId4VcVerifier.createProofRequest({ + redirectUri: 'http://redirect-uri', + verificationMethod, + }) + ).rejects.toThrow() + }) + + it(`check openid proof request format`, async () => { + const { proofRequest } = await agent.modules.openId4VcVerifier.createProofRequest({ + redirectUri: 'http://redirect-uri', + verificationMethod, + holderMetadata: staticOpOpenIdConfigEDDSA, + presentationDefinition: universityDegreePresentationDefinition, + }) + + const base = + 'openid://?redirect_uri=http%3A%2F%2Fredirect-uri&presentation_definition=%7B%22id%22%3A%22UniversityDegreeCredential%22%2C%22input_descriptors%22%3A%5B%7B%22id%22%3A%22UniversityDegree%22%2C%22format%22%3A%7B%22jwt_vc%22%3A%7B%22alg%22%3A%5B%22EdDSA%22%5D%7D%7D%2C%22constraints%22%3A%7B%22fields%22%3A%5B%7B%22path%22%3A%5B%22%24.vc.type.*%22%5D%2C%22filter%22%3A%7B%22type%22%3A%22string%22%2C%22pattern%22%3A%22UniversityDegree%22%7D%7D%5D%7D%7D%5D%7D&request=' + expect(proofRequest.startsWith(base)).toBe(true) + + const _jwt = proofRequest.substring(base.length) + const jwt = Jwt.fromSerializedJwt(_jwt) + + expect(proofRequest.startsWith(base)).toBe(true) + + expect(jwt.header.kid).toEqual(kid) + expect(jwt.header.alg).toEqual(SigningAlgo.EDDSA) + expect(jwt.header.typ).toEqual('JWT') + expect(jwt.payload.additionalClaims.scope).toEqual('openid') + expect(jwt.payload.additionalClaims.client_id).toEqual(kid) + expect(jwt.payload.additionalClaims.redirect_uri).toEqual('http://redirect-uri') + expect(jwt.payload.additionalClaims.response_mode).toEqual('post') + expect(jwt.payload.additionalClaims.nonce).toBeDefined() + expect(jwt.payload.additionalClaims.state).toBeDefined() + expect(jwt.payload.additionalClaims.response_type).toEqual('id_token vp_token') + expect(jwt.payload.iss).toEqual(did) + expect(jwt.payload.sub).toEqual(did) + }) + + it(`check siop proof request format`, async () => { + const { proofRequest } = await agent.modules.openId4VcVerifier.createProofRequest({ + redirectUri: 'http://redirect-uri', + verificationMethod, + holderMetadata: staticSiopConfigEDDSA, + }) + + // TODO: this should be siopv2 + const base = 'openid://?redirect_uri=http%3A%2F%2Fredirect-uri&request=' + expect(proofRequest.startsWith(base)).toBe(true) + + const _jwt = proofRequest.substring(base.length) + const jwt = Jwt.fromSerializedJwt(_jwt) + + expect(jwt.header.kid).toEqual(kid) + expect(jwt.header.alg).toEqual(SigningAlgo.EDDSA) + expect(jwt.payload.additionalClaims.scope).toEqual('openid') + expect(jwt.payload.additionalClaims.client_id).toEqual(kid) + expect(jwt.payload.additionalClaims.redirect_uri).toEqual('http://redirect-uri') + expect(jwt.payload.additionalClaims.response_mode).toEqual('post') + expect(jwt.payload.additionalClaims.response_type).toEqual('id_token') + expect(jwt.payload.additionalClaims.nonce).toBeDefined() + expect(jwt.payload.additionalClaims.state).toBeDefined() + expect(jwt.payload.iss).toEqual(did) + expect(jwt.payload.sub).toEqual(did) }) }) }) From 6baf500da6cf0a0f373fcf5cc8929fdd765abf99 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 20 Nov 2023 13:52:00 +0100 Subject: [PATCH 062/115] fix: export types and jwt_vc_json-ld --- .../src/OpenId4VcIssuerApi.ts | 32 +++---- .../src/OpenId4VcIssuerService.ts | 13 ++- .../src/OpenId4VcIssuerServiceOptions.ts | 25 +++-- .../tests/openid4vc-issuer.e2e.test.ts | 94 ++++++++++++++----- 4 files changed, 111 insertions(+), 53 deletions(-) diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts index 2d8aee71cb..d71d73c610 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts @@ -1,10 +1,10 @@ import type { CreateIssueCredentialResponseOptions, - CreateCredentialOfferOptions, + CreateCredentialOfferAndRequestOptions, CredentialOfferAndRequest, OfferedCredential, + CredentialOffer, } from './OpenId4VcIssuerServiceOptions' -import type { CredentialOfferPayload } from '@sphereon/oid4vci-common' import { injectable, AgentContext } from '@aries-framework/core' @@ -30,19 +30,19 @@ export class OpenId4VcIssuerApi { * Creates a credential offer, and credential offer request. * Either the preAuthorizedCodeFlowConfig or the authorizationCodeFlowConfig must be provided. * - * @param {OfferedCredential[]} offeredCredentials - The credentials to be offered. - * @param {IssuerMetadata} options.issuerMetadata - Metadata about the issuer. - * @param {string} options.credentialOfferUri - The URI to retrieve the credential offer if the offer is passed by reference. - * @param {string} options.scheme - The credential offer request scheme. Default is https. - * @param {string} options.baseUri - The base URI of the credential offer request. - * @param {PreAuthorizedCodeFlowConfig} options.preAuthorizedCodeFlowConfig - The configuration for the pre-authorized code flow. This or the authorizationCodeFlowConfig must be provided. - * @param {AuthorizationCodeFlowConfig} options.authorizationCodeFlowConfig - The configuration for the authorization code flow. This or the preAuthorizedCodeFlowConfig must be provided. + * @param offeredCredentials - The credentials to be offered. + * @param options.issuerMetadata - Metadata about the issuer. + * @param options.credentialOfferUri - The URI to retrieve the credential offer if the offer is passed by reference. + * @param options.scheme - The credential offer request scheme. Default is https. + * @param options.baseUri - The base URI of the credential offer request. + * @param options.preAuthorizedCodeFlowConfig - The configuration for the pre-authorized code flow. This or the authorizationCodeFlowConfig must be provided. + * @param options.authorizationCodeFlowConfig - The configuration for the authorization code flow. This or the preAuthorizedCodeFlowConfig must be provided. * - * @returns {CredentialOfferAndRequest} Object containing the payload of the credential offer and the credential offer request, which is to be sent to the wallet. + * @returns Object containing the payload of the credential offer and the credential offer request, which is to be sent to the wallet. */ - public async createCredentialOffer( + public async createCredentialOfferAndRequest( offeredCredentials: OfferedCredential[], - options: CreateCredentialOfferOptions + options: CreateCredentialOfferAndRequestOptions ): Promise { return await this.openId4VcIssuerService.createCredentialOffer(this.agentContext, offeredCredentials, options) } @@ -50,13 +50,13 @@ export class OpenId4VcIssuerApi { /** * This function retrieves a credential offer from a given URI. * Retrieving a credential offer from a URI is possible after a credential offer was created with - * @see createCredentialOffer and the credentialOfferUri option. + * @see createCredentialOfferAndRequest and the credentialOfferUri option. * * @throws if no credential offer can found for the given URI. - * @param {string} uri - The URI for which to retrieve the credential offer. - * @returns {CredentialOfferPayload} - The credential offer payload associated with the given URI. + * @param uri - The URI for which to retrieve the credential offer. + * @returns The credential offer payload associated with the given URI. */ - public async getCredentialOfferFromUri(uri: string): Promise { + public async getCredentialOfferFromUri(uri: string): Promise { return await this.openId4VcIssuerService.getCredentialOfferFromUri(uri) } diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts index e2191465f3..c6119faf34 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts @@ -1,10 +1,11 @@ import type { - CreateCredentialOfferOptions, + CreateCredentialOfferAndRequestOptions, AuthorizationCodeFlowConfig, PreAuthorizedCodeFlowConfig, OfferedCredential, IssuerMetadata, CreateIssueCredentialResponseOptions, + CredentialSupported, } from './OpenId4VcIssuerServiceOptions' import type { AgentContext, @@ -17,7 +18,6 @@ import type { CredentialRequestJwtVc, CredentialRequestLdpVc, Grant, - CredentialSupported, MetadataDisplay, JWTVerifyCallback, CredentialOfferFormat, @@ -27,6 +27,7 @@ import type { CNonceState, CredentialOfferSession, URIState, + CredentialSupported as SphereonCredentialSupported, } from '@sphereon/oid4vci-common' import type { CredentialDataSupplier, @@ -187,7 +188,6 @@ export class OpenId4VcIssuerService { // If the Credential shall be bound to a DID, the kid refers to a DID URL which identifies a // particular key in the DID Document that the Credential shall be bound to. - // TODO: proofpurpose const holderVerificationMethod = holderDidDocument.dereferenceKey(kid, ['assertionMethod', 'assertionMethod']) let signed: W3cVerifiableCredential @@ -228,7 +228,8 @@ export class OpenId4VcIssuerService { .withCredentialIssuer(credentialIssuer) .withCredentialEndpoint(credentialEndpoint) .withTokenEndpoint(tokenEndpoint) - .withCredentialsSupported(credentialsSupported) + // FIXME: currently credentialsSupported is not typed correctly + .withCredentialsSupported(credentialsSupported as SphereonCredentialSupported[]) .withCNonceExpiresIn(this.cNonceExpiresIn) // 5 minutes .withCNonceStateManager(this._cNonceStateManager) .withCredentialOfferStateManager(this._credentialOfferSessionManager) @@ -327,7 +328,7 @@ export class OpenId4VcIssuerService { public async createCredentialOffer( agentContext: AgentContext, offeredCredentials: OfferedCredential[], - options: CreateCredentialOfferOptions + options: CreateCredentialOfferAndRequestOptions ) { const { preAuthorizedCodeFlowConfig, authorizationCodeFlowConfig } = options @@ -403,8 +404,6 @@ export class OpenId4VcIssuerService { offeredCredential.credential_definition.types, credentialRequest.credential_definition.types ) - } else { - throw new AriesFrameworkError(`Unsupported credential format ${credentialRequest.format}.`) } }) } diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts index 65e5ffefad..5553ccfc5b 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts @@ -1,15 +1,25 @@ import type { VerificationMethod, W3cCredential } from '@aries-framework/core' import type { - CredentialOfferFormat, + CommonCredentialSupported, CredentialOfferPayloadV1_0_11, CredentialRequestV1_0_11, - CredentialResponse, - CredentialSupported, MetadataDisplay, ProofOfPossession, } from '@sphereon/oid4vci-common' -export { CredentialResponse } +export type { MetadataDisplay, ProofOfPossession } + +export type CredentialFormatSupported = 'jwt_vc_json' | 'jwt_vc_json-ld' + +export interface CredentialOfferFormat { + format: CredentialFormatSupported + types: string[] +} + +export interface CredentialSupported extends CommonCredentialSupported { + format: CredentialFormatSupported + types: string[] +} // If the entry is an object, the object contains the data related to a certain credential type // the Wallet MAY request. Each object MUST contain a format Claim determining the format @@ -36,12 +46,11 @@ export type IssuerMetadata = { credentialsSupported: CredentialSupported[] } -export interface CreateCredentialOfferOptions { +export interface CreateCredentialOfferAndRequestOptions { // The scheme used for the credentialIssuer. Default is https scheme?: 'http' | 'https' | 'openid-credential-offer' | string // The base URI of the credential offer uri - // TODO: rename to credentialOfferRequestBaseUri baseUri: string preAuthorizedCodeFlowConfig?: PreAuthorizedCodeFlowConfig @@ -52,10 +61,10 @@ export interface CreateCredentialOfferOptions { issuerMetadata?: IssuerMetadata } -export type CredentialOfferPayload = CredentialOfferPayloadV1_0_11 +export type CredentialOffer = CredentialOfferPayloadV1_0_11 export type CredentialOfferAndRequest = { - credentialOffer: CredentialOfferPayload + credentialOffer: CredentialOffer credentialOfferRequest: string } diff --git a/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts b/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts index 06c7767a98..9454377fcb 100644 --- a/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts +++ b/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts @@ -3,6 +3,7 @@ import type { AuthorizationCodeFlowConfig, IssuerMetadata, CredentialRequest, + CredentialSupported, } from '../src/OpenId4VcIssuerServiceOptions' import type { AgentContext, @@ -11,7 +12,6 @@ import type { W3cVerifiableCredential, W3cVerifyCredentialResult, } from '@aries-framework/core' -import type { CredentialSupported } from '@sphereon/oid4vci-common' import type { OriginalVerifiableCredential as SphereonW3cVerifiableCredential } from '@sphereon/ssi-types' import { AskarModule } from '@aries-framework/askar' @@ -53,6 +53,12 @@ const universityDegreeCredential: CredentialSupported & { id: string } = { types: ['VerifiableCredential', 'UniversityDegreeCredential'], } +const universityDegreeCredentialLd: CredentialSupported & { id: string } = { + id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredentialLd', + format: 'jwt_vc_json-ld', + types: ['VerifiableCredential', 'UniversityDegreeCredential'], +} + const baseCredentialRequestOptions = { scheme: 'openid-credential-offer', baseUri: 'openid4vc-issuer.com', @@ -62,7 +68,7 @@ const issuerMetadata: IssuerMetadata = { credentialIssuer: 'https://openid4vc-issuer.com', credentialEndpoint: 'https://openid4vc-issuer.com/credentials', tokenEndpoint: 'https://openid4vc-issuer.com/token', - credentialsSupported: [openBadgeCredential], + credentialsSupported: [openBadgeCredential, universityDegreeCredentialLd], } const modules = { @@ -76,7 +82,7 @@ const createCredentialRequestFromKid = async ( agentContext: AgentContext, options: { issuerMetadata: IssuerMetadata - format: 'jwt_vc_json' + format: 'jwt_vc_json' | 'jwt_vc_json-ld' types: string[] nonce: string kid: string @@ -114,8 +120,6 @@ const createCredentialRequestFromKid = async ( key, }) - // TODO: check different proof types - return { format, types, @@ -135,7 +139,7 @@ async function handleCredentialResponse( let w3cVerifiableCredential: W3cVerifiableCredential if (typeof sphereonVerifiableCredential === 'string') { - if (format !== 'jwt_vc_json' && format !== 'ldp_vc') throw new Error('Invalid format') + if (format !== 'jwt_vc_json' && format !== 'jwt_vc_json-ld') throw new Error('Invalid format') // validate json-ld credentials w3cVerifiableCredential = W3cJwtVerifiableCredential.fromSerializedJwt(sphereonVerifiableCredential) result = await w3cCredentialService.verifyCredential(agentContext, { credential: w3cVerifiableCredential }) @@ -245,7 +249,7 @@ describe('OpenId4VcIssuer', () => { const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, userPinRequired: false } - const result = await issuer.modules.openId4VcIssuer.createCredentialOffer([openBadgeCredential.id], { + const result = await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest([openBadgeCredential.id], { preAuthorizedCodeFlowConfig, ...baseCredentialRequestOptions, }) @@ -293,7 +297,7 @@ describe('OpenId4VcIssuer', () => { const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, userPinRequired: false } await expect( - issuer.modules.openId4VcIssuer.createCredentialOffer(['invalid id'], { + issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest(['invalid id'], { //issuerMetadata: { // ...baseIssuerMetadata, // credentialsSupported: [openBadgeCredential, universityDegreeCredential], @@ -312,7 +316,7 @@ describe('OpenId4VcIssuer', () => { const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, userPinRequired: false } - const result = await issuer.modules.openId4VcIssuer.createCredentialOffer([openBadgeCredential.id], { + const result = await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest([openBadgeCredential.id], { preAuthorizedCodeFlowConfig, ...baseCredentialRequestOptions, }) @@ -343,6 +347,56 @@ describe('OpenId4VcIssuer', () => { ).rejects.toThrowError() }) + it('pre authorized code flow using multiple credentials_supported', async () => { + const cNonce = '1234' + const preAuthorizedCode = '1234567890' + + await issuerService.cNonceStateManager.set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) + + const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, userPinRequired: false } + + const result = await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest( + [openBadgeCredential.id, universityDegreeCredentialLd.id], + { + preAuthorizedCodeFlowConfig, + ...baseCredentialRequestOptions, + } + ) + + expect(result.credentialOfferRequest).toEqual( + 'openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%2C%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FUniversityDegreeCredentialLd%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%22%7D' + ) + + const credential = new W3cCredential({ + type: universityDegreeCredentialLd.types, + issuer: new W3cIssuer({ id: issuerDid }), + credentialSubject: new W3cCredentialSubject({ id: holderDid }), + issuanceDate: w3cDate(Date.now()), + }) + + const issueCredentialResponse = await issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ + credential, + verificationMethod: issuerVerificationMethod, + credentialRequest: await createCredentialRequestFromKid(holder.context, { + format: universityDegreeCredentialLd.format, + types: universityDegreeCredentialLd.types, + issuerMetadata, + kid: holderKid, + nonce: cNonce, + }), + }) + + const sphereonW3cCredential = issueCredentialResponse.credential + if (!sphereonW3cCredential) throw new Error('No credential found') + + await handleCredentialResponse( + holder.context, + sphereonW3cCredential, + universityDegreeCredentialLd.format, + universityDegreeCredential.types + ) + }) + it('requesting non offered credential errors', async () => { const cNonce = '1234' const preAuthorizedCode = '1234567890' @@ -354,7 +408,7 @@ describe('OpenId4VcIssuer', () => { userPinRequired: false, } - const result = await issuer.modules.openId4VcIssuer.createCredentialOffer([openBadgeCredential.id], { + const result = await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest([openBadgeCredential.id], { preAuthorizedCodeFlowConfig, ...baseCredentialRequestOptions, }) @@ -393,7 +447,7 @@ describe('OpenId4VcIssuer', () => { const authorizationCodeFlowConfig: AuthorizationCodeFlowConfig = { issuerState } - const result = await issuer.modules.openId4VcIssuer.createCredentialOffer([openBadgeCredential.id], { + const result = await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest([openBadgeCredential.id], { authorizationCodeFlowConfig, ...baseCredentialRequestOptions, }) @@ -440,14 +494,12 @@ describe('OpenId4VcIssuer', () => { const credentialOfferUri = 'https://openid4vc-issuer.com/credential-offer-uri' - const { credentialOfferRequest, credentialOffer } = await issuer.modules.openId4VcIssuer.createCredentialOffer( - [openBadgeCredential.id], - { + const { credentialOfferRequest, credentialOffer } = + await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest([openBadgeCredential.id], { ...baseCredentialRequestOptions, credentialOfferUri, preAuthorizedCodeFlowConfig, - } - ) + }) expect(credentialOfferRequest).toEqual( `openid-credential-offer://openid4vc-issuer.com?credential_offer_uri=${credentialOfferUri}` @@ -464,14 +516,12 @@ describe('OpenId4VcIssuer', () => { const authorizationCodeFlowConfig: AuthorizationCodeFlowConfig = { issuerState: '1234567890' } const credentialOfferUri = 'https://openid4vc-issuer.com/credential-offer-uri' - const { credentialOfferRequest, credentialOffer } = await issuer.modules.openId4VcIssuer.createCredentialOffer( - [openBadgeCredential.id], - { + const { credentialOfferRequest, credentialOffer } = + await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest([openBadgeCredential.id], { ...baseCredentialRequestOptions, credentialOfferUri, authorizationCodeFlowConfig, - } - ) + }) expect(credentialOfferRequest).toEqual( `openid-credential-offer://openid4vc-issuer.com?credential_offer_uri=${credentialOfferUri}` @@ -493,7 +543,7 @@ describe('OpenId4VcIssuer', () => { const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, userPinRequired: false } - const result = await issuer.modules.openId4VcIssuer.createCredentialOffer( + const result = await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest( [ openBadgeCredential.id, { From 7035265e3916609ad7146f73e355aab3ca2ecf56 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 20 Nov 2023 14:17:30 +0100 Subject: [PATCH 063/115] fix: export types remove proofRequest type --- .../src/OpenId4VcVerifierServiceOptions.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts index 06215ca594..b2d519ed76 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts @@ -17,7 +17,7 @@ export { PassBy, SigningAlgo, SubjectType, ResponseType, Scope } from '@sphereon export type HolderMetadata = ClientMetadataOpts & { authorization_endpoint?: string } -export { PresentationDefinitionV1, PresentationDefinitionV2 } from '@sphereon/pex-models' +export type { PresentationDefinitionV1, PresentationDefinitionV2, VerifiedOpenID4VPSubmission, IDTokenPayload } export interface CreateProofRequestOptions { verificationMethod: VerificationMethod @@ -27,8 +27,6 @@ export interface CreateProofRequestOptions { presentationDefinition?: PresentationDefinitionV1 | PresentationDefinitionV2 } -export type ProofRequest = string - export interface ProofRequestMetadata { correlationId: string challenge: string @@ -36,7 +34,7 @@ export interface ProofRequestMetadata { } export type ProofRequestWithMetadata = { - proofRequest: ProofRequest + proofRequest: string proofRequestMetadata: ProofRequestMetadata } From 9b5dd42cdb3251a5081bd40090b32c7268ecde9c Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 20 Nov 2023 14:33:43 +0100 Subject: [PATCH 064/115] fix: export types and move type definitions --- .../src/OpenId4VcHolderApi.ts | 2 +- .../presentation/OpenId4VpHolderService.ts | 15 +++-------- .../OpenId4VpHolderServiceOptions.ts | 20 ++++++++++---- .../tests/openid4vp-holder.e2e.test.ts | 27 +++++++++---------- 4 files changed, 32 insertions(+), 32 deletions(-) diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts index 1afe5cb3d4..d15a17a7fa 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts @@ -82,7 +82,7 @@ export class OpenId4VcHolderApi { const { submission, submissionEntryIndexes } = presentation return await this.openId4VpHolderService.acceptProofRequest(this.agentContext, presentationRequest, { submission, - submissionEntryIndexes: submissionEntryIndexes, + submissionEntryIndexes, }) } diff --git a/packages/openid4vc-holder/src/presentation/OpenId4VpHolderService.ts b/packages/openid4vc-holder/src/presentation/OpenId4VpHolderService.ts index 3d131d3d54..32bad0f8f5 100644 --- a/packages/openid4vc-holder/src/presentation/OpenId4VpHolderService.ts +++ b/packages/openid4vc-holder/src/presentation/OpenId4VpHolderService.ts @@ -7,7 +7,7 @@ import type { import type { PresentationSubmission } from './selection' import type { InputDescriptorToCredentials } from './selection/types' import type { AgentContext, VerificationMethod, W3cVerifiablePresentation } from '@aries-framework/core' -import type { PresentationDefinitionWithLocation, VerifiedAuthorizationRequest } from '@sphereon/did-auth-siop' +import type { VerifiedAuthorizationRequest } from '@sphereon/did-auth-siop' import type { W3CVerifiablePresentation } from '@sphereon/ssi-types' import { @@ -35,16 +35,9 @@ import { getResolver, getSuppliedSignatureFromVerificationMethod, getSupportedDi import { PresentationExchangeService } from './PresentationExchangeService' -/** - * SIOPv2 Authorization Request with a single v1 / v2 presentation definition - */ -export type VerifiedAuthorizationRequestWithPresentationDefinition = VerifiedAuthorizationRequest & { - presentationDefinitions: [PresentationDefinitionWithLocation] -} - function isVerifiedAuthorizationRequestWithPresentationDefinition( request: VerifiedAuthorizationRequest -): request is VerifiedAuthorizationRequestWithPresentationDefinition { +): request is PresentationRequest { return ( request.presentationDefinitions !== undefined && request.presentationDefinitions.length === 1 && @@ -122,7 +115,7 @@ export class OpenId4VpHolderService { // which means you should never continue the authentication flow! const presentationDefs = verifiedAuthorizationRequest.presentationDefinitions if (!presentationDefs || presentationDefs.length === 0) { - return { proofType: 'authentication', authenticationRequest: verifiedAuthorizationRequest } + return { proofType: 'authentication', request: verifiedAuthorizationRequest } } // FIXME: I don't see any reason why we would support multiple presentation definitions @@ -140,7 +133,7 @@ export class OpenId4VpHolderService { presentationDefinition ) - return { proofType: 'presentation', presentationRequest: verifiedAuthorizationRequest, presentationSubmission } + return { proofType: 'presentation', request: verifiedAuthorizationRequest, presentationSubmission } } /** diff --git a/packages/openid4vc-holder/src/presentation/OpenId4VpHolderServiceOptions.ts b/packages/openid4vc-holder/src/presentation/OpenId4VpHolderServiceOptions.ts index 7a0252b219..4a09fb0be1 100644 --- a/packages/openid4vc-holder/src/presentation/OpenId4VpHolderServiceOptions.ts +++ b/packages/openid4vc-holder/src/presentation/OpenId4VpHolderServiceOptions.ts @@ -1,14 +1,24 @@ -import type { PresentationSubmission, VerifiedAuthorizationRequestWithPresentationDefinition } from '..' -import type { AuthorizationResponsePayload, VerifiedAuthorizationRequest } from '@sphereon/did-auth-siop' +import type { PresentationSubmission } from './selection' +import type { + AuthorizationResponsePayload, + PresentationDefinitionWithLocation, + VerifiedAuthorizationRequest, +} from '@sphereon/did-auth-siop' export type AuthenticationRequest = VerifiedAuthorizationRequest -export type PresentationRequest = VerifiedAuthorizationRequestWithPresentationDefinition + +/** + * SIOPv2 Authorization Request with a single v1 / v2 presentation definition + */ +export type PresentationRequest = VerifiedAuthorizationRequest & { + presentationDefinitions: [PresentationDefinitionWithLocation] +} export type ResolvedProofRequest = - | { proofType: 'authentication'; authenticationRequest: AuthenticationRequest } + | { proofType: 'authentication'; request: AuthenticationRequest } | { proofType: 'presentation' - presentationRequest: PresentationRequest + request: PresentationRequest presentationSubmission: PresentationSubmission } diff --git a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts index 8f5c521c59..53913c30c8 100644 --- a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts @@ -164,12 +164,12 @@ describe('OpenId4VcHolder | OpenID4VP', () => { //////////////////////////// OP (accept the verified request) //////////////////////////// const { submittedResponse } = await holder.modules.openId4VcHolder.acceptAuthenticationRequest( - result.authenticationRequest, + result.request, holderVerificationMethod ) - expect(result.authenticationRequest.authorizationRequestPayload.redirect_uri).toBe('https://acme.com/hello') - expect(result.authenticationRequest.issuer).toBe(verifierVerificationMethod.controller) + expect(result.request.authorizationRequestPayload.redirect_uri).toBe('https://acme.com/hello') + expect(result.request.issuer).toBe(verifierVerificationMethod.controller) //////////////////////////// RP (verify the response) //////////////////////////// @@ -221,7 +221,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { //////////////////////////// OP (accept the verified request) //////////////////////////// const { submittedResponse } = await holder.modules.openId4VcHolder.acceptAuthenticationRequest( - result.authenticationRequest, + result.request, holderVerificationMethod ) @@ -310,7 +310,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { if (resolvedUniversityDegree.proofType !== 'presentation') throw new Error('expected prooftype presentation') await expect( - holder.modules.openId4VcHolder.acceptPresentationRequest(resolvedOpenBadge.presentationRequest, { + holder.modules.openId4VcHolder.acceptPresentationRequest(resolvedOpenBadge.request, { submission: resolvedUniversityDegree.presentationSubmission, submissionEntryIndexes: [0], }) @@ -340,7 +340,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const result = await holder.modules.openId4VcHolder.resolveProofRequest(proofRequest) if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') - const { presentationRequest, presentationSubmission } = result + const { request: presentationRequest, presentationSubmission } = result expect(presentationSubmission.areRequirementsSatisfied).toBeTruthy() expect(presentationSubmission.requirements.length).toBe(1) expect(presentationSubmission.requirements[0].needsCount).toBe(1) @@ -388,13 +388,10 @@ describe('OpenId4VcHolder | OpenID4VP', () => { expect(presentationSubmission.requirements[0].submissionEntry[0].inputDescriptorId).toBe('OpenBadgeCredential') expect(presentationSubmission.requirements[1].submissionEntry[0].inputDescriptorId).toBe('UniversityDegree') - const { submittedResponse } = await holder.modules.openId4VcHolder.acceptPresentationRequest( - result.presentationRequest, - { - submission: result.presentationSubmission, - submissionEntryIndexes: [0, 0], - } - ) + const { submittedResponse } = await holder.modules.openId4VcHolder.acceptPresentationRequest(result.request, { + submission: result.presentationSubmission, + submissionEntryIndexes: [0, 0], + }) const { idTokenPayload, submission } = await verifier.modules.openId4VcVerifier.verifyProofResponse( submittedResponse, @@ -441,7 +438,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') await expect( - holder.modules.openId4VcHolder.acceptPresentationRequest(result.presentationRequest, { + holder.modules.openId4VcHolder.acceptPresentationRequest(result.request, { submission: result.presentationSubmission, submissionEntryIndexes: [0], }) @@ -479,7 +476,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { //////////////////////////// OP (accept the verified request) //////////////////////////// const { submittedResponse, status } = await holder.modules.openId4VcHolder.acceptPresentationRequest( - result.presentationRequest, + result.request, { submission: result.presentationSubmission, submissionEntryIndexes: [0], From 356497e82597b7bdcae5ce20258a08501a54495c Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 20 Nov 2023 15:04:47 +0100 Subject: [PATCH 065/115] fix: remove and rename types --- .../src/OpenId4VcHolderApi.ts | 2 +- .../src/issuance/OpenId4VciHolderService.ts | 7 ++- .../OpenId4VciHolderServiceOptions.ts | 54 ++----------------- 3 files changed, 9 insertions(+), 54 deletions(-) diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts index d15a17a7fa..eff4c21605 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts @@ -3,11 +3,11 @@ import type { ResolvedAuthorizationRequest, AuthCodeFlowOptions, AcceptCredentialOfferOptions, + CredentialOfferPayloadV1_0_11, } from './issuance/OpenId4VciHolderServiceOptions' import type { PresentationSubmission } from './presentation' import type { AuthenticationRequest, PresentationRequest } from './presentation/OpenId4VpHolderServiceOptions' import type { VerificationMethod, W3cCredentialRecord } from '@aries-framework/core' -import type { CredentialOfferPayloadV1_0_11 } from '@sphereon/oid4vci-common' import { injectable, AgentContext } from '@aries-framework/core' diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts index b4948b474d..393dda1be6 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts @@ -4,10 +4,10 @@ import type { AcceptCredentialOfferOptions, ProofOfPossessionRequirements, ProofOfPossessionVerificationMethodResolver, - SupportedCredentialFormats, ResolvedCredentialOffer, ResolvedAuthorizationRequest, ResolvedAuthorizationRequestWithCode, + SupportedCredentialFormats, } from './OpenId4VciHolderServiceOptions' import type { OfferedCredentialWithMetadata } from './utils/IssuerMetadataUtils' import type { @@ -72,7 +72,6 @@ import { import { getSupportedJwaSignatureAlgorithms } from '../shared' -import { supportedCredentialFormats } from './OpenId4VciHolderServiceOptions' import { OpenIdCredentialFormatProfile, fromOpenIdCredentialFormatProfileToDifClaimFormat } from './utils' import { getFormatForVersion, getUniformFormat } from './utils/Formats' import { @@ -205,7 +204,7 @@ export class OpenId4VciHolderService { public async resolveCredentialOffer( credentialOffer: UniformCredentialOfferPayload | string, opts?: { version?: OpenId4VCIVersion } - ) { + ): Promise { let version = opts?.version ?? OpenId4VCIVersion.VER_1_0_11 const claimedCredentialOfferUrl = `openid-credential-offer://?` const claimedIssuanceInitiationUrl = `openid-initiate-issuance://?` @@ -463,7 +462,7 @@ export class OpenId4VciHolderService { for (const credentialWithMetadata of credentialsToRequestWithMetadata ?? offeredCredentialsWithMetadata) { // Get all options for the credential request (such as which kid to use, the signature algorithm, etc) const { verificationMethod, signatureAlgorithm } = await this.getCredentialRequestOptions(agentContext, { - allowedCredentialFormats: supportedCredentialFormats, + allowedCredentialFormats: [OpenIdCredentialFormatProfile.JwtVcJson, OpenIdCredentialFormatProfile.JwtVcJsonLd], allowedProofOfPossessionSignatureAlgorithms, offeredCredentialWithMetadata: credentialWithMetadata, proofOfPossessionVerificationMethodResolver, diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderServiceOptions.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderServiceOptions.ts index f4fe7599b1..10c345a257 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderServiceOptions.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderServiceOptions.ts @@ -1,15 +1,13 @@ import type { OfferedCredentialType } from './utils/IssuerMetadataUtils' +import type { OpenIdCredentialFormatProfile } from './utils/claimFormatMapping' import type { JwaSignatureAlgorithm, KeyType, VerificationMethod } from '@aries-framework/core' import type { CredentialOfferPayloadV1_0_11, EndpointMetadataResult, OpenId4VCIVersion } from '@sphereon/oid4vci-common' -import { OpenIdCredentialFormatProfile } from './utils/claimFormatMapping' +export type SupportedCredentialFormats = + | OpenIdCredentialFormatProfile.JwtVcJson + | OpenIdCredentialFormatProfile.JwtVcJsonLd -export type SupportedCredentialFormats = OpenIdCredentialFormatProfile.JwtVcJson | OpenIdCredentialFormatProfile.LdpVc - -export const supportedCredentialFormats = [ - OpenIdCredentialFormatProfile.JwtVcJson, - OpenIdCredentialFormatProfile.LdpVc, -] satisfies OpenIdCredentialFormatProfile[] +export type { OfferedCredentialType, OpenId4VCIVersion, EndpointMetadataResult, CredentialOfferPayloadV1_0_11 } export type CredentialToRequest = { format: string; types: string[] } & ( | { offerType: OfferedCredentialType.InlineCredentialOffer } @@ -174,45 +172,3 @@ export interface ProofOfPossessionRequirements { supportedDidMethods?: string[] supportsAllDidMethods: boolean } - -/** - * @internal - */ -export enum AuthFlowType { - AuthorizationCodeFlow, - PreAuthorizedCodeFlow, -} - -type WithInternalOptions = Options & { - flowType: FlowType - - /** - * The endpoint metadata received from the credential issuer. - * This is obtained manually or by calling the `resolveCredentialOffer` method. - */ - metadata?: EndpointMetadataResult - - /** - * The resolved credential offer payload that was received from the issuer. - * This is obtained manually or by calling the `resolveCredentialOffer` method. - */ - credentialOfferPayload: CredentialOfferPayloadV1_0_11 - - /** - * The openid4vci specification version. - * This is obtained manually or by calling the `resolveCredentialOffer` method. - */ - version: OpenId4VCIVersion -} - -export type AuthorizationCodeFlowOptions = WithInternalOptions -export type PreAuthorizedCodeFlowOptions = WithInternalOptions< - AuthFlowType.PreAuthorizedCodeFlow, - AcceptCredentialOfferOptions -> - -/** - * The options that are used to request a credential from an issuer. - * @internal - */ -export type RequestCredentialOptions = PreAuthorizedCodeFlowOptions & AuthorizationCodeFlowOptions From 7059dcbc52e77d19352c0d3b9fa97036c511b1bf Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 20 Nov 2023 16:10:24 +0100 Subject: [PATCH 066/115] fix: rename types, fix test, rename method --- .../openid4vc-issuer/src/OpenId4VcIssuerApi.ts | 10 +++++++--- .../src/OpenId4VcIssuerService.ts | 7 ++++--- .../src/OpenId4VcIssuerServiceOptions.ts | 7 +++---- .../tests/openid4vc-issuer.e2e.test.ts | 15 +++++++-------- 4 files changed, 21 insertions(+), 18 deletions(-) diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts index d71d73c610..935ce00581 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts @@ -3,7 +3,7 @@ import type { CreateCredentialOfferAndRequestOptions, CredentialOfferAndRequest, OfferedCredential, - CredentialOffer, + CredentialOfferPayloadV1_0_11, } from './OpenId4VcIssuerServiceOptions' import { injectable, AgentContext } from '@aries-framework/core' @@ -44,7 +44,11 @@ export class OpenId4VcIssuerApi { offeredCredentials: OfferedCredential[], options: CreateCredentialOfferAndRequestOptions ): Promise { - return await this.openId4VcIssuerService.createCredentialOffer(this.agentContext, offeredCredentials, options) + return await this.openId4VcIssuerService.createCredentialOfferAndReqeust( + this.agentContext, + offeredCredentials, + options + ) } /** @@ -56,7 +60,7 @@ export class OpenId4VcIssuerApi { * @param uri - The URI for which to retrieve the credential offer. * @returns The credential offer payload associated with the given URI. */ - public async getCredentialOfferFromUri(uri: string): Promise { + public async getCredentialOfferFromUri(uri: string): Promise { return await this.openId4VcIssuerService.getCredentialOfferFromUri(uri) } diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts index c6119faf34..d76c4d0a61 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts @@ -6,6 +6,7 @@ import type { IssuerMetadata, CreateIssueCredentialResponseOptions, CredentialSupported, + CredentialOfferAndRequest, } from './OpenId4VcIssuerServiceOptions' import type { AgentContext, @@ -325,11 +326,11 @@ export class OpenId4VcIssuerService { return [...credentialsReferencingCredentialsSupported, ...inlineCredentialOffers] } - public async createCredentialOffer( + public async createCredentialOfferAndReqeust( agentContext: AgentContext, offeredCredentials: OfferedCredential[], options: CreateCredentialOfferAndRequestOptions - ) { + ): Promise { const { preAuthorizedCodeFlowConfig, authorizationCodeFlowConfig } = options const issuerMetadata = options.issuerMetadata ?? this.issuerMetadata @@ -351,7 +352,7 @@ export class OpenId4VcIssuerService { }) return { - credentialOffer: session.credentialOffer.credential_offer, + credentialOfferPayload: session.credentialOffer.credential_offer, credentialOfferRequest: uri, } } diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts index 5553ccfc5b..4be06292a1 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts @@ -7,8 +7,9 @@ import type { ProofOfPossession, } from '@sphereon/oid4vci-common' -export type { MetadataDisplay, ProofOfPossession } +export type { MetadataDisplay, ProofOfPossession, CredentialOfferPayloadV1_0_11 } +// TODO: duplicate export type CredentialFormatSupported = 'jwt_vc_json' | 'jwt_vc_json-ld' export interface CredentialOfferFormat { @@ -61,10 +62,8 @@ export interface CreateCredentialOfferAndRequestOptions { issuerMetadata?: IssuerMetadata } -export type CredentialOffer = CredentialOfferPayloadV1_0_11 - export type CredentialOfferAndRequest = { - credentialOffer: CredentialOffer + credentialOfferPayload: CredentialOfferPayloadV1_0_11 credentialOfferRequest: string } diff --git a/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts b/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts index 9454377fcb..b307482549 100644 --- a/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts +++ b/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts @@ -494,7 +494,7 @@ describe('OpenId4VcIssuer', () => { const credentialOfferUri = 'https://openid4vc-issuer.com/credential-offer-uri' - const { credentialOfferRequest, credentialOffer } = + const { credentialOfferRequest, credentialOfferPayload } = await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest([openBadgeCredential.id], { ...baseCredentialRequestOptions, credentialOfferUri, @@ -509,14 +509,14 @@ describe('OpenId4VcIssuer', () => { credentialOfferUri ) - expect(credentialOffer).toEqual(credentialOfferReceivedByUri) + expect(credentialOfferPayload).toEqual(credentialOfferReceivedByUri) }) it('create credential offer and retrieve it from the uri (authorizationCodeFlow)', async () => { const authorizationCodeFlowConfig: AuthorizationCodeFlowConfig = { issuerState: '1234567890' } const credentialOfferUri = 'https://openid4vc-issuer.com/credential-offer-uri' - const { credentialOfferRequest, credentialOffer } = + const { credentialOfferRequest, credentialOfferPayload } = await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest([openBadgeCredential.id], { ...baseCredentialRequestOptions, credentialOfferUri, @@ -531,11 +531,10 @@ describe('OpenId4VcIssuer', () => { credentialOfferUri ) - expect(credentialOffer).toEqual(credentialOfferReceivedByUri) + expect(credentialOfferPayload).toEqual(credentialOfferReceivedByUri) }) - // https://github.com/orgs/hyperledger/projects/32/views/1?pane=issue&itemId=44709598 - xit('offer and request multiple credentials', async () => { + it('offer and request multiple credentials', async () => { const cNonce = '1234' const preAuthorizedCode = '1234567890' @@ -605,7 +604,7 @@ describe('OpenId4VcIssuer', () => { types: universityDegreeCredential.types, issuerMetadata, kid: holderKid, - nonce: cNonce, + nonce: issueCredentialResponse.c_nonce ?? cNonce, }), }) @@ -614,7 +613,7 @@ describe('OpenId4VcIssuer', () => { await handleCredentialResponse( holder.context, - sphereonW3cCredential, + sphereonW3cCredential2, universityDegreeCredential.format, universityDegreeCredential.types ) From 5ce1794789f58495f3b3ea44b1c37876fa316cb3 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 20 Nov 2023 16:20:02 +0100 Subject: [PATCH 067/115] fix: todos --- .../src/presentation/PresentationExchangeService.ts | 1 - packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts | 2 ++ packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts | 5 ++--- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/openid4vc-holder/src/presentation/PresentationExchangeService.ts b/packages/openid4vc-holder/src/presentation/PresentationExchangeService.ts index 4d178a08a2..2f13983e60 100644 --- a/packages/openid4vc-holder/src/presentation/PresentationExchangeService.ts +++ b/packages/openid4vc-holder/src/presentation/PresentationExchangeService.ts @@ -361,7 +361,6 @@ export class PresentationExchangeService { return this.getSigningAlgorithmFromVerificationMethod(verificationMethod, suitableAlgorithms) } - // TODO: is this a proper implementation? private getProofTypeForLdpVc( agentContext: AgentContext, presentationDefinition: IPresentationDefinition, diff --git a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts index 53913c30c8..fa53f5157c 100644 --- a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts @@ -16,6 +16,8 @@ import { waltPortalOpenBadgeJwt, waltUniversityDegreeJwt } from './fixtures_vp' // id id%22%3A%22test%22%2C%22 // * = %2A +// TODO: use proper credential + // TODO: error on sphereon lib PR opened // TODO: walt issued credentials verification fails due to some time issue || //throw new Error(`Inconsistent issuance dates between JWT claim (${nbfDateAsStr}) and VC value (${issuanceDate})`); // TODO: error walt no id in presentation definition diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts index d76c4d0a61..58d6ebb2fb 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts @@ -118,7 +118,6 @@ export class OpenId4VcIssuerService { this._uriStateManager = openId4VcIssuerModuleConfig.uriStateManager ?? new MemoryStates() } - // TODO: check if this is correct private getProofTypeForLdpVc(agentContext: AgentContext, verificationMethod: VerificationMethod) { const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) @@ -189,7 +188,7 @@ export class OpenId4VcIssuerService { // If the Credential shall be bound to a DID, the kid refers to a DID URL which identifies a // particular key in the DID Document that the Credential shall be bound to. - const holderVerificationMethod = holderDidDocument.dereferenceKey(kid, ['assertionMethod', 'assertionMethod']) + const holderVerificationMethod = holderDidDocument.dereferenceKey(kid, ['assertionMethod']) let signed: W3cVerifiableCredential if (format === 'jwt_vc_json' || format === 'jwt_vc_json-ld') { @@ -204,7 +203,7 @@ export class OpenId4VcIssuerService { format: ClaimFormat.LdpVc, credential: W3cCredential.fromJson(credential), verificationMethod: issuerVerificationMethod.id, - proofPurpose: 'authentication', // TODO: is it authentication? + proofPurpose: 'assertionMethod', proofType: this.getProofTypeForLdpVc(agentContext, holderVerificationMethod), }) } From eb7884df79bbd53b1cc213bce8dd249698a81314 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 22 Nov 2023 10:17:52 +0100 Subject: [PATCH 068/115] feat: restructure verifier, add verifier endpoint, add verifier session manager --- packages/openid4vc-holder/package.json | 16 +- .../src/OpenId4VcHolderApi.ts | 5 +- .../src/OpenId4VcHolderModule.ts | 3 +- .../src/issuance/OpenId4VciHolderService.ts | 27 +- packages/openid4vc-holder/src/presentation.ts | 9 + packages/openid4vc-holder/src/shared.ts | 86 ----- .../tests/openId4vc-holder-module.test.ts | 7 +- .../tests/openid4vci-holder.e2e.test.ts | 6 +- .../tests/openid4vp-holder.e2e.test.ts | 191 +++++++--- packages/openid4vc-verifier/package.json | 9 +- .../src/InMemoryVerifierSessionManager.ts | 341 ++++++++++++++++++ .../src/OpenId4VcVerifierApi.ts | 4 +- .../src/OpenId4VcVerifierModule.ts | 44 ++- .../src/OpenId4VcVerifierModuleConfig.ts | 32 ++ .../src/OpenId4VcVerifierService.ts | 93 +++-- packages/openid4vc-verifier/src/index.ts | 8 + .../presentation/OpenId4VpHolderService.ts | 0 .../OpenId4VpHolderServiceOptions.ts | 0 .../PresentationExchangeService.ts | 0 .../src/presentation/index.ts | 0 .../selection/PexCredentialSelection.ts | 0 .../src/presentation/selection/example.md | 0 .../src/presentation/selection/index.ts | 0 .../src/presentation/selection/types.ts | 0 .../src/presentation/transform.ts | 0 .../tests/openId4vc-verifier-module.test.ts | 4 +- .../tests/openid4vc-verifier.e2e.test.ts | 2 +- yarn.lock | 68 +++- 28 files changed, 743 insertions(+), 212 deletions(-) create mode 100644 packages/openid4vc-holder/src/presentation.ts delete mode 100644 packages/openid4vc-holder/src/shared.ts create mode 100644 packages/openid4vc-verifier/src/InMemoryVerifierSessionManager.ts create mode 100644 packages/openid4vc-verifier/src/OpenId4VcVerifierModuleConfig.ts rename packages/{openid4vc-holder => openid4vc-verifier}/src/presentation/OpenId4VpHolderService.ts (100%) rename packages/{openid4vc-holder => openid4vc-verifier}/src/presentation/OpenId4VpHolderServiceOptions.ts (100%) rename packages/{openid4vc-holder => openid4vc-verifier}/src/presentation/PresentationExchangeService.ts (100%) rename packages/{openid4vc-holder => openid4vc-verifier}/src/presentation/index.ts (100%) rename packages/{openid4vc-holder => openid4vc-verifier}/src/presentation/selection/PexCredentialSelection.ts (100%) rename packages/{openid4vc-holder => openid4vc-verifier}/src/presentation/selection/example.md (100%) rename packages/{openid4vc-holder => openid4vc-verifier}/src/presentation/selection/index.ts (100%) rename packages/{openid4vc-holder => openid4vc-verifier}/src/presentation/selection/types.ts (100%) rename packages/{openid4vc-holder => openid4vc-verifier}/src/presentation/transform.ts (100%) diff --git a/packages/openid4vc-holder/package.json b/packages/openid4vc-holder/package.json index 512ba2ec81..808e54999b 100644 --- a/packages/openid4vc-holder/package.json +++ b/packages/openid4vc-holder/package.json @@ -21,26 +21,22 @@ "clean": "rimraf ./build", "compile": "tsc -p tsconfig.build.json", "prepublishOnly": "yarn run build", - "test": "jest" + "test": "jest --forceExit --detectOpenHandles" }, "dependencies": { "@aries-framework/askar": "^0.4.2", "@aries-framework/core": "0.4.2", - "@sphereon/did-auth-siop": "^0.5.0-unstable.7", + "@aries-framework/openid4vc-verifier": "0.4.2", "@sphereon/oid4vci-client": "^0.8.1", - "@sphereon/oid4vci-common": "^0.8.1", - "@sphereon/pex": "2.2.0", - "@sphereon/pex-models": "^2.1.1", - "@sphereon/ssi-types": "^0.17.5", - "jsonpath": "1.1.1" + "@sphereon/oid4vci-common": "^0.8.1" }, "devDependencies": { "@aries-framework/node": "^0.4.2", "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", - "@types/jsonpath": "^0.2.1", + "@types/express": "^4.17.21", + "express": "^4.18.2", "nock": "^13.3.0", "rimraf": "^4.4.0", - "typescript": "~4.9.5", - "@aries-framework/openid4vc-verifier": "0.4.2" + "typescript": "~4.9.5" } } diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts index eff4c21605..e76cb0c8b5 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts @@ -5,14 +5,13 @@ import type { AcceptCredentialOfferOptions, CredentialOfferPayloadV1_0_11, } from './issuance/OpenId4VciHolderServiceOptions' -import type { PresentationSubmission } from './presentation' -import type { AuthenticationRequest, PresentationRequest } from './presentation/OpenId4VpHolderServiceOptions' +import type { AuthenticationRequest, PresentationRequest, PresentationSubmission } from './presentation' import type { VerificationMethod, W3cCredentialRecord } from '@aries-framework/core' import { injectable, AgentContext } from '@aries-framework/core' +import { OpenId4VpHolderService } from '@aries-framework/openid4vc-verifier' import { OpenId4VciHolderService } from './issuance/OpenId4VciHolderService' -import { OpenId4VpHolderService } from './presentation' /** * @public diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts b/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts index 075a919280..e50031cb87 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts @@ -1,11 +1,10 @@ import type { DependencyManager, Module } from '@aries-framework/core' import { AgentConfig } from '@aries-framework/core' +import { OpenId4VpHolderService, PresentationExchangeService } from '@aries-framework/openid4vc-verifier' import { OpenId4VcHolderApi } from './OpenId4VcHolderApi' import { OpenId4VciHolderService } from './issuance/OpenId4VciHolderService' -import { PresentationExchangeService } from './presentation' -import { OpenId4VpHolderService } from './presentation/OpenId4VpHolderService' /** * @public @module OpenId4VcHolderModule diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts index 393dda1be6..96b7dfc1b3 100644 --- a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts +++ b/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts @@ -53,6 +53,7 @@ import { injectable, parseDid, equalsIgnoreOrder, + getJwkClassFromKeyType, } from '@aries-framework/core' import { AccessTokenClient, @@ -70,8 +71,6 @@ import { JsonURIMode, } from '@sphereon/oid4vci-common' -import { getSupportedJwaSignatureAlgorithms } from '../shared' - import { OpenIdCredentialFormatProfile, fromOpenIdCredentialFormatProfileToDifClaimFormat } from './utils' import { getFormatForVersion, getUniformFormat } from './utils/Formats' import { @@ -81,6 +80,30 @@ import { OfferedCredentialType, } from './utils/IssuerMetadataUtils' +// TODO: duplicate +/** + * Returns the JWA Signature Algorithms that are supported by the wallet. + * + * This is an approximation based on the supported key types of the wallet. + * This is not 100% correct as a supporting a key type does not mean you support + * all the algorithms for that key type. However, this needs refactoring of the wallet + * that is planned for the 0.5.0 release. + */ +export function getSupportedJwaSignatureAlgorithms(agentContext: AgentContext): JwaSignatureAlgorithm[] { + const supportedKeyTypes = agentContext.wallet.supportedKeyTypes + + // Extract the supported JWS algs based on the key types the wallet support. + const supportedJwaSignatureAlgorithms = supportedKeyTypes + // Map the supported key types to the supported JWK class + .map(getJwkClassFromKeyType) + // Filter out the undefined values + .filter((jwkClass): jwkClass is Exclude => jwkClass !== undefined) + // Extract the supported JWA signature algorithms from the JWK class + .flatMap((jwkClass) => jwkClass.supportedSignatureAlgorithms) + + return supportedJwaSignatureAlgorithms +} + export interface AuthDetails { type: 'openid_credential' | string locations?: string | string[] diff --git a/packages/openid4vc-holder/src/presentation.ts b/packages/openid4vc-holder/src/presentation.ts new file mode 100644 index 0000000000..515ed9e525 --- /dev/null +++ b/packages/openid4vc-holder/src/presentation.ts @@ -0,0 +1,9 @@ +import type { Presentation } from '@aries-framework/openid4vc-verifier' + +export type AuthenticationRequest = Presentation.AuthenticationRequest +export type PresentationRequest = Presentation.PresentationRequest +export type PresentationSubmission = Presentation.PresentationSubmission +export type ProofSubmissionResponse = Presentation.ProofSubmissionResponse +export type ResolvedProofRequest = Presentation.ResolvedProofRequest +export type SubmissionEntry = Presentation.SubmissionEntry +export type VpFormat = Presentation.VpFormat diff --git a/packages/openid4vc-holder/src/shared.ts b/packages/openid4vc-holder/src/shared.ts deleted file mode 100644 index c514f10dbe..0000000000 --- a/packages/openid4vc-holder/src/shared.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { AgentContext, VerificationMethod, JwaSignatureAlgorithm } from '@aries-framework/core' -import type { DIDDocument, SigningAlgo } from '@sphereon/did-auth-siop' - -import { - AriesFrameworkError, - DidsApi, - TypedArrayEncoder, - getKeyFromVerificationMethod, - getJwkClassFromKeyType, -} from '@aries-framework/core' - -/** - * Returns the JWA Signature Algorithms that are supported by the wallet. - * - * This is an approximation based on the supported key types of the wallet. - * This is not 100% correct as a supporting a key type does not mean you support - * all the algorithms for that key type. However, this needs refactoring of the wallet - * that is planned for the 0.5.0 release. - */ -export function getSupportedJwaSignatureAlgorithms(agentContext: AgentContext): JwaSignatureAlgorithm[] { - const supportedKeyTypes = agentContext.wallet.supportedKeyTypes - - // Extract the supported JWS algs based on the key types the wallet support. - const supportedJwaSignatureAlgorithms = supportedKeyTypes - // Map the supported key types to the supported JWK class - .map(getJwkClassFromKeyType) - // Filter out the undefined values - .filter((jwkClass): jwkClass is Exclude => jwkClass !== undefined) - // Extract the supported JWA signature algorithms from the JWK class - .flatMap((jwkClass) => jwkClass.supportedSignatureAlgorithms) - - return supportedJwaSignatureAlgorithms -} - -export function getSupportedDidMethods(agentContext: AgentContext) { - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - const supportedDidMethods: Set = new Set() - - for (const resolver of didsApi.config.resolvers) { - resolver.supportedMethods.forEach((method) => supportedDidMethods.add(method)) - } - - return Array.from(supportedDidMethods) -} - -export async function getSuppliedSignatureFromVerificationMethod( - agentContext: AgentContext, - verificationMethod: VerificationMethod -) { - // get the key from the verification method and use the first supported signature algorithm - const key = getKeyFromVerificationMethod(verificationMethod) - const alg = getJwkClassFromKeyType(key.keyType)?.supportedSignatureAlgorithms[0] - if (!alg) throw new AriesFrameworkError(`No supported signature algorithms for key type: ${key.keyType}`) - - const suppliedSignature = { - signature: async (data: string | Uint8Array) => { - if (typeof data !== 'string') throw new AriesFrameworkError("Expected string but received 'Uint8Array'") - const signedData = await agentContext.wallet.sign({ - data: TypedArrayEncoder.fromString(data), - key, - }) - - const signature = TypedArrayEncoder.toBase64URL(signedData) - return signature - }, - alg: alg as unknown as SigningAlgo, - did: verificationMethod.controller, - kid: verificationMethod.id, - } - - return suppliedSignature -} - -export function getResolver(agentContext: AgentContext) { - return { - resolve: async (didUrl: string) => { - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - const result = await didsApi.resolve(didUrl) - - return { - ...result, - didDocument: result.didDocument?.toJSON() as DIDDocument, - } - }, - } -} diff --git a/packages/openid4vc-holder/tests/openId4vc-holder-module.test.ts b/packages/openid4vc-holder/tests/openId4vc-holder-module.test.ts index 8ea31c0ed0..014bc09c80 100644 --- a/packages/openid4vc-holder/tests/openId4vc-holder-module.test.ts +++ b/packages/openid4vc-holder/tests/openId4vc-holder-module.test.ts @@ -1,10 +1,11 @@ /* eslint-disable @typescript-eslint/unbound-method */ import type { DependencyManager } from '@aries-framework/core' +import { Presentation } from '@aries-framework/openid4vc-verifier' + import { OpenId4VcHolderApi } from '../src/OpenId4VcHolderApi' import { OpenId4VcHolderModule } from '../src/OpenId4VcHolderModule' import { OpenId4VciHolderService } from '../src/issuance/OpenId4VciHolderService' -import { OpenId4VpHolderService, PresentationExchangeService } from '../src/presentation' const dependencyManager = { registerInstance: jest.fn(), @@ -23,7 +24,7 @@ describe('OpenId4VcHolderModule', () => { expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(3) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VciHolderService) - expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VpHolderService) - expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(PresentationExchangeService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(Presentation.OpenId4VpHolderService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(Presentation.PresentationExchangeService) }) }) diff --git a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts index 2fba95c320..93a2830c0b 100644 --- a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts @@ -39,10 +39,10 @@ describe('OpenId4VcHolder', () => { beforeEach(async () => { agent = new Agent({ config: { - label: 'OpenId4VcHolder Test20', + label: 'OpenId4VcHolder Test23', walletConfig: { - id: 'openid4vc-holder-test21', - key: 'openid4vc-holder-test22', + id: 'openid4vc-holder-test24', + key: 'openid4vc-holder-test25', }, }, dependencies: agentDependencies, diff --git a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts index fa53f5157c..a2fc6dc8eb 100644 --- a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts @@ -1,13 +1,15 @@ import type { KeyDidCreateOptions, VerificationMethod } from '@aries-framework/core' import type { CreateProofRequestOptions } from '@aries-framework/openid4vc-verifier' import type { PresentationDefinitionV2 } from '@sphereon/pex-models' +import type { Express } from 'express' +import type { Server } from 'http' import { AskarModule } from '@aries-framework/askar' -import { KeyType, Agent, TypedArrayEncoder, DidKey, W3cJwtVerifiableCredential } from '@aries-framework/core' +import { Agent, DidKey, KeyType, TypedArrayEncoder, W3cJwtVerifiableCredential } from '@aries-framework/core' import { agentDependencies } from '@aries-framework/node' -import { OpenId4VcVerifierModule, staticOpOpenIdConfig } from '@aries-framework/openid4vc-verifier' +import { OpenId4VcVerifierModule, SigningAlgo, staticOpOpenIdConfig } from '@aries-framework/openid4vc-verifier' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' -import { SigningAlgo } from '@sphereon/did-auth-siop' +import express from 'express' import nock from 'nock' import { OpenId4VcHolderModule } from '../src' @@ -70,47 +72,99 @@ const staticOpOpenIdConfigEdDSA = { vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] }, jwt_vp: { alg: [SigningAlgo.EDDSA] } }, } -const modules = { - openId4VcHolder: new OpenId4VcHolderModule(), - openId4VcVerifier: new OpenId4VcVerifierModule(), - askar: new AskarModule({ ariesAskar }), +const port = 3121 +const verificationEndpointPath = '/proofResponse' +const verificationEndpoint = `http://localhost:${port}${verificationEndpointPath}` + +const createHolderModules = () => { + const modules = { + openId4VcHolder: new OpenId4VcHolderModule(), + askar: new AskarModule({ ariesAskar }), + } + + return modules } describe('OpenId4VcHolder | OpenID4VP', () => { - let verifier: Agent + let verifier: Agent let verifierVerificationMethod: VerificationMethod + let verifierApp: Express + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let verifierServer: Server - let holder: Agent + let holder: Agent let holderVerificationMethod: VerificationMethod + const mockFunction = jest.fn() + mockFunction.mockReturnValue({ status: 200 }) + + function waitForMockFunction() { + return new Promise((resolve, reject) => { + const intervalId = setInterval(() => { + if (mockFunction.mock.calls.length > 0) { + clearInterval(intervalId) + resolve(0) + } + }, 100) + + setTimeout(() => { + clearInterval(intervalId) + reject(new Error('Timeout Callback')) + }, 10000) + }) + } + + const createVerifierModules = (verifierApp: Express) => { + const modules = { + openId4VcHolder: new OpenId4VcHolderModule(), + openId4VcVerifier: new OpenId4VcVerifierModule({ + endPointConfig: { + app: verifierApp, + verificationEndpointPath, + proofResponseHandler: mockFunction, + }, + }), + + askar: new AskarModule({ ariesAskar }), + } + + return modules + } + + type VerifierModules = ReturnType + type HolderModules = ReturnType + beforeEach(async () => { + verifierApp = express() verifier = new Agent({ config: { - label: 'OpenId4VcRp OpenID4VP Test36', + label: 'OpenId4VcRp OpenID4VP Test39', walletConfig: { - id: 'openid4vc-rp-openid4vp-test37', - key: 'openid4vc-rp-openid4vp-test38', + id: 'openid4vc-rp-openid4vp-test40', + key: 'openid4vc-rp-openid4vp-test41', }, }, dependencies: agentDependencies, - modules, + modules: createVerifierModules(verifierApp), }) holder = new Agent({ config: { - label: 'OpenId4VcOp OpenID4VP Test37', + label: 'OpenId4VcOp OpenID4VP Test39', walletConfig: { - id: 'openid4vc-op-openid4vp-test38', - key: 'openid4vc-op-openid4vp-test39', + id: 'openid4vc-op-openid4vp-test40', + key: 'openid4vc-op-openid4vp-test41', }, }, dependencies: agentDependencies, - modules, + modules: createHolderModules(), }) await verifier.initialize() await holder.initialize() + verifierServer = verifierApp.listen(port) + const verifierDid = await verifier.dids.create({ method: 'key', options: { keyType: KeyType.Ed25519 }, @@ -139,6 +193,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { }) afterEach(async () => { + verifierServer.close() await holder.shutdown() await holder.wallet.delete() await verifier.shutdown() @@ -148,7 +203,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { it('siop request with static metadata', async () => { const createProofRequestOptions: CreateProofRequestOptions = { verificationMethod: verifierVerificationMethod, - redirectUri: 'https://acme.com/hello', + redirectUri: verificationEndpoint, holderMetadata: staticOpOpenIdConfigEdDSA, } @@ -165,22 +220,20 @@ describe('OpenId4VcHolder | OpenID4VP', () => { if (result.proofType == 'presentation') throw new Error('Expected an authenticationRequest') //////////////////////////// OP (accept the verified request) //////////////////////////// - const { submittedResponse } = await holder.modules.openId4VcHolder.acceptAuthenticationRequest( + const { submittedResponse, status } = await holder.modules.openId4VcHolder.acceptAuthenticationRequest( result.request, holderVerificationMethod ) - expect(result.request.authorizationRequestPayload.redirect_uri).toBe('https://acme.com/hello') + expect(status).toBe(200) + + expect(result.request.authorizationRequestPayload.redirect_uri).toBe(verificationEndpoint) expect(result.request.issuer).toBe(verifierVerificationMethod.controller) //////////////////////////// RP (verify the response) //////////////////////////// const { idTokenPayload, submission } = await verifier.modules.openId4VcVerifier.verifyProofResponse( - submittedResponse, - { - createProofRequestOptions, - proofRequestMetadata, - } + submittedResponse ) const { state, challenge } = proofRequestMetadata @@ -188,6 +241,12 @@ describe('OpenId4VcHolder | OpenID4VP', () => { expect(idTokenPayload).toBeDefined() expect(idTokenPayload.state).toMatch(state) expect(idTokenPayload.nonce).toMatch(challenge) + + await waitForMockFunction() + expect(mockFunction).toBeCalledWith({ + idTokenPayload: expect.objectContaining(idTokenPayload), + submission: undefined, + }) }) // TODO: not working yet @@ -204,7 +263,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const createProofRequestOptions: CreateProofRequestOptions = { verificationMethod: verifierVerificationMethod, - redirectUri: 'https://acme.com/hello', + redirectUri: verificationEndpoint, // TODO: if provided this way client metadata is not resolved for the verification method holderIdentifier: 'https://helloworld.com', } @@ -222,28 +281,35 @@ describe('OpenId4VcHolder | OpenID4VP', () => { if (result.proofType == 'presentation') throw new Error('Expected a proofType') //////////////////////////// OP (accept the verified request) //////////////////////////// - const { submittedResponse } = await holder.modules.openId4VcHolder.acceptAuthenticationRequest( + const { submittedResponse, status } = await holder.modules.openId4VcHolder.acceptAuthenticationRequest( result.request, holderVerificationMethod ) + expect(status).toBe(200) + //////////////////////////// RP (verify the response) //////////////////////////// - const verifiedProofPresponse = await verifier.modules.openId4VcVerifier.verifyProofResponse(submittedResponse, { - createProofRequestOptions, - proofRequestMetadata, - }) + const { idTokenPayload, submission } = await verifier.modules.openId4VcVerifier.verifyProofResponse( + submittedResponse + ) const { state, challenge } = proofRequestMetadata - expect(verifiedProofPresponse.idTokenPayload).toBeDefined() - expect(verifiedProofPresponse.idTokenPayload.state).toMatch(state) - expect(verifiedProofPresponse.idTokenPayload.nonce).toMatch(challenge) + expect(idTokenPayload).toBeDefined() + expect(idTokenPayload.state).toMatch(state) + expect(idTokenPayload.nonce).toMatch(challenge) + + await waitForMockFunction() + expect(mockFunction).toBeCalledWith({ + idTokenPayload: expect.objectContaining(idTokenPayload), + submission: expect.objectContaining(submission), + }) }) it('resolving vp request with no credentials', async () => { const createProofRequestOptions: CreateProofRequestOptions = { verificationMethod: verifierVerificationMethod, - redirectUri: 'https://acme.com/hello', + redirectUri: verificationEndpoint, holderMetadata: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, } @@ -265,7 +331,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const createProofRequestOptions: CreateProofRequestOptions = { verificationMethod: verifierVerificationMethod, - redirectUri: 'https://acme.com/hello', + redirectUri: verificationEndpoint, holderMetadata: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, } @@ -291,7 +357,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const createProofRequestOptions: CreateProofRequestOptions = { verificationMethod: verifierVerificationMethod, - redirectUri: 'https://acme.com/hello', + redirectUri: verificationEndpoint, holderMetadata: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, } @@ -330,7 +396,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const createProofRequestOptions: CreateProofRequestOptions = { verificationMethod: verifierVerificationMethod, - redirectUri: 'https://acme.com/hello', + redirectUri: verificationEndpoint, holderMetadata: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, } @@ -363,7 +429,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const createProofRequestOptions: CreateProofRequestOptions = { verificationMethod: verifierVerificationMethod, - redirectUri: 'https://acme.com/hello', + redirectUri: verificationEndpoint, holderMetadata: staticOpOpenIdConfigEdDSA, presentationDefinition: combinePresentationDefinitions([ openBadgePresentationDefinition, @@ -371,9 +437,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { ]), } - const { proofRequest, proofRequestMetadata } = await verifier.modules.openId4VcVerifier.createProofRequest( - createProofRequestOptions - ) + const { proofRequest } = await verifier.modules.openId4VcVerifier.createProofRequest(createProofRequestOptions) //////////////////////////// OP (validate and parse the request) //////////////////////////// @@ -390,19 +454,20 @@ describe('OpenId4VcHolder | OpenID4VP', () => { expect(presentationSubmission.requirements[0].submissionEntry[0].inputDescriptorId).toBe('OpenBadgeCredential') expect(presentationSubmission.requirements[1].submissionEntry[0].inputDescriptorId).toBe('UniversityDegree') - const { submittedResponse } = await holder.modules.openId4VcHolder.acceptPresentationRequest(result.request, { - submission: result.presentationSubmission, - submissionEntryIndexes: [0, 0], - }) - - const { idTokenPayload, submission } = await verifier.modules.openId4VcVerifier.verifyProofResponse( - submittedResponse, + const { submittedResponse, status } = await holder.modules.openId4VcHolder.acceptPresentationRequest( + result.request, { - createProofRequestOptions, - proofRequestMetadata, + submission: result.presentationSubmission, + submissionEntryIndexes: [0, 0], } ) + expect(status).toBe(200) + + const { idTokenPayload, submission } = await verifier.modules.openId4VcVerifier.verifyProofResponse( + submittedResponse + ) + expect(idTokenPayload).toBeDefined() expect(submission).toBeDefined() expect(submission?.presentationDefinitions).toHaveLength(1) @@ -411,6 +476,12 @@ describe('OpenId4VcHolder | OpenID4VP', () => { expect(submission?.presentations[0].vcs).toHaveLength(2) expect(submission?.presentations[0].vcs[0].credential.type).toContain('OpenBadgeCredential') expect(submission?.presentations[0].vcs[1].credential.type).toContain('UniversityDegree') + + await waitForMockFunction() + expect(mockFunction).toBeCalledWith({ + idTokenPayload: expect.objectContaining(idTokenPayload), + submission: expect.objectContaining(submission), + }) }) it('expect accepting a proof request with only a partial set of requirements to error', async () => { @@ -424,7 +495,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const createProofRequestOptions: CreateProofRequestOptions = { verificationMethod: verifierVerificationMethod, - redirectUri: 'https://acme.com/hello', + redirectUri: verificationEndpoint, holderMetadata: staticOpOpenIdConfigEdDSA, presentationDefinition: combinePresentationDefinitions([ openBadgePresentationDefinition, @@ -454,7 +525,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const createProofRequestOptions: CreateProofRequestOptions = { verificationMethod: verifierVerificationMethod, - redirectUri: 'https://acme.com/hello', + redirectUri: verificationEndpoint, holderMetadata: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, } @@ -485,17 +556,13 @@ describe('OpenId4VcHolder | OpenID4VP', () => { } ) - expect(status).toBe(404) + expect(status).toBe(200) // The RP MUST validate that the aud (audience) Claim contains the value of the client_id // that the RP sent in the Authorization Request as an audience. // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. const { idTokenPayload, submission } = await verifier.modules.openId4VcVerifier.verifyProofResponse( - submittedResponse, - { - createProofRequestOptions, - proofRequestMetadata, - } + submittedResponse ) const { state, challenge } = proofRequestMetadata @@ -508,6 +575,12 @@ describe('OpenId4VcHolder | OpenID4VP', () => { expect(submission?.submissionData.definition_id).toBe('OpenBadgeCredential') expect(submission?.presentations).toHaveLength(1) expect(submission?.presentations[0].vcs[0].credential.type).toContain('OpenBadgeCredential') + + await waitForMockFunction() + expect(mockFunction).toBeCalledWith({ + idTokenPayload: expect.objectContaining(idTokenPayload), + submission: expect.objectContaining(submission), + }) }) // it('edited walt vp request', async () => { diff --git a/packages/openid4vc-verifier/package.json b/packages/openid4vc-verifier/package.json index f1be2f7ebf..d4598453d9 100644 --- a/packages/openid4vc-verifier/package.json +++ b/packages/openid4vc-verifier/package.json @@ -27,11 +27,18 @@ "@aries-framework/askar": "^0.4.2", "@aries-framework/core": "0.4.2", "@sphereon/did-auth-siop": "^0.5.0-unstable.7", - "@sphereon/pex-models": "^2.1.1" + "@sphereon/pex": "2.2.0", + "@sphereon/pex-models": "^2.1.1", + "@sphereon/ssi-types": "^0.17.5", + "@types/jsonpath": "^0.2.4", + "body-parser": "^1.20.2" }, "devDependencies": { "@aries-framework/node": "^0.4.2", "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", + "@types/body-parser": "^1.19.5", + "@types/express": "^4.17.21", + "jsonpath": "1.1.1", "nock": "^13.3.0", "rimraf": "^4.4.0", "typescript": "~4.9.5" diff --git a/packages/openid4vc-verifier/src/InMemoryVerifierSessionManager.ts b/packages/openid4vc-verifier/src/InMemoryVerifierSessionManager.ts new file mode 100644 index 0000000000..b9334f0841 --- /dev/null +++ b/packages/openid4vc-verifier/src/InMemoryVerifierSessionManager.ts @@ -0,0 +1,341 @@ +import type { VerifyProofResponseOptions } from './OpenId4VcVerifierServiceOptions' +import type { Logger } from '@aries-framework/core' +import type { + AuthorizationEvent, + AuthorizationRequest, + AuthorizationRequestState, + AuthorizationResponse, + AuthorizationResponseState, + IRPSessionManager as SphereonRPSessionManager, +} from '@sphereon/did-auth-siop' +import type { PresentationDefinitionV1, PresentationDefinitionV2 } from '@sphereon/pex-models' +import type { EventEmitter } from 'events' + +import { AriesFrameworkError } from '@aries-framework/core' +import { + AuthorizationEvents, + AuthorizationRequestStateStatus, + AuthorizationResponseStateStatus, +} from '@sphereon/did-auth-siop' + +export type PresentationDefinitionForCorrelationId = + | { + proofType: 'presentation' + presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2 + } + | { + proofType: 'authentication' + } + +export interface IInMemoryVerifierSessionManager extends SphereonRPSessionManager { + getVerifiyProofResponseOptions(correlationId: string): Promise + saveVerifyProofResponseOptions( + correlationId: string, + presentationDefinitionForCorrelationId: VerifyProofResponseOptions + ): Promise +} + +/** + * Please note that this session manager is not really meant to be used in large production settings, as it stores everything in memory! + * It also doesn't do scheduled cleanups. It runs a cleanup whenever a request or response is received. In a high-volume production setting you will want scheduled cleanups running in the background + * Since this is a low level library we have not created a full-fledged implementation. + * We suggest to create your own implementation using the event system of the library + */ +export class InMemoryVerifierSessionManager implements IInMemoryVerifierSessionManager { + private readonly authorizationRequests: Record = {} + private readonly authorizationResponses: Record = {} + private readonly logger: Logger + + private readonly nonceToCorrelationId: Record = {} + + private readonly stateToCorrelationId: Record = {} + + private readonly correlationIdToVerifyProofResponseOptions: Record = {} + + private readonly maxAgeInSeconds: number + + private static getKeysForCorrelationId(mapping: Record, correlationId: string): number[] { + return Object.entries(mapping) + .filter((entry) => entry[1] === correlationId) + .map((filtered) => Number.parseInt(filtered[0])) + } + + public constructor(eventEmitter: EventEmitter, logger: Logger, opts?: { maxAgeInSeconds?: number }) { + this.logger = logger + this.maxAgeInSeconds = opts?.maxAgeInSeconds ?? 5 * 60 + eventEmitter.on( + AuthorizationEvents.ON_AUTH_REQUEST_CREATED_SUCCESS, + this.onAuthorizationRequestCreatedSuccess.bind(this) + ) + eventEmitter.on( + AuthorizationEvents.ON_AUTH_REQUEST_CREATED_FAILED, + this.onAuthorizationRequestCreatedFailed.bind(this) + ) + eventEmitter.on(AuthorizationEvents.ON_AUTH_REQUEST_SENT_SUCCESS, this.onAuthorizationRequestSentSuccess.bind(this)) + eventEmitter.on(AuthorizationEvents.ON_AUTH_REQUEST_SENT_FAILED, this.onAuthorizationRequestSentFailed.bind(this)) + eventEmitter.on( + AuthorizationEvents.ON_AUTH_RESPONSE_RECEIVED_SUCCESS, + this.onAuthorizationResponseReceivedSuccess.bind(this) + ) + eventEmitter.on( + AuthorizationEvents.ON_AUTH_RESPONSE_RECEIVED_FAILED, + this.onAuthorizationResponseReceivedFailed.bind(this) + ) + eventEmitter.on( + AuthorizationEvents.ON_AUTH_RESPONSE_VERIFIED_SUCCESS, + this.onAuthorizationResponseVerifiedSuccess.bind(this) + ) + eventEmitter.on( + AuthorizationEvents.ON_AUTH_RESPONSE_VERIFIED_FAILED, + this.onAuthorizationResponseVerifiedFailed.bind(this) + ) + } + public async getVerifiyProofResponseOptions(correlationId: string): Promise { + return this.correlationIdToVerifyProofResponseOptions[correlationId] + } + + public async saveVerifyProofResponseOptions( + correlationId: string, + verifyProofResponseOptions: VerifyProofResponseOptions + ) { + await this.cleanup() + this.correlationIdToVerifyProofResponseOptions[correlationId] = verifyProofResponseOptions + } + + public async getRequestStateByCorrelationId( + correlationId: string, + errorOnNotFound?: boolean + ): Promise { + return await this.getFromMapping('correlationId', correlationId, this.authorizationRequests, errorOnNotFound) + } + + public async getRequestStateByNonce( + nonce: string, + errorOnNotFound?: boolean + ): Promise { + return await this.getFromMapping('nonce', nonce, this.authorizationRequests, errorOnNotFound) + } + + public async getRequestStateByState( + state: string, + errorOnNotFound?: boolean + ): Promise { + return await this.getFromMapping('state', state, this.authorizationRequests, errorOnNotFound) + } + + public async getResponseStateByCorrelationId( + correlationId: string, + errorOnNotFound?: boolean + ): Promise { + return await this.getFromMapping('correlationId', correlationId, this.authorizationResponses, errorOnNotFound) + } + + public async getResponseStateByNonce( + nonce: string, + errorOnNotFound?: boolean + ): Promise { + return await this.getFromMapping('nonce', nonce, this.authorizationResponses, errorOnNotFound) + } + + public async getResponseStateByState( + state: string, + errorOnNotFound?: boolean + ): Promise { + return await this.getFromMapping('state', state, this.authorizationResponses, errorOnNotFound) + } + + private async getFromMapping( + type: 'nonce' | 'state' | 'correlationId', + value: string, + mapping: Record, + errorOnNotFound?: boolean + ): Promise { + const correlationId = + type === 'correlationId' ? value : await this.getCorrelationIdImpl(type, value, errorOnNotFound) + if (!correlationId) throw new AriesFrameworkError(`Could not find ${type} from correlation id ${correlationId}`) + + const result = mapping[correlationId] + if (!result && errorOnNotFound) + throw new AriesFrameworkError(`Could not find ${type} from correlation id ${correlationId}`) + return result + } + + private async onAuthorizationRequestCreatedSuccess(event: AuthorizationEvent): Promise { + this.updateState('request', event, AuthorizationRequestStateStatus.CREATED).catch((error) => + this.logger.error(JSON.stringify(error)) + ) + } + + private async onAuthorizationRequestCreatedFailed(event: AuthorizationEvent): Promise { + this.updateState('request', event, AuthorizationRequestStateStatus.ERROR).catch((error) => + this.logger.error(JSON.stringify(error)) + ) + } + + private async onAuthorizationRequestSentSuccess(event: AuthorizationEvent): Promise { + this.updateState('request', event, AuthorizationRequestStateStatus.SENT).catch((error) => + this.logger.error(JSON.stringify(error)) + ) + } + + private async onAuthorizationRequestSentFailed(event: AuthorizationEvent): Promise { + this.updateState('request', event, AuthorizationRequestStateStatus.ERROR).catch((error) => + this.logger.error(JSON.stringify(error)) + ) + } + + private async onAuthorizationResponseReceivedSuccess( + event: AuthorizationEvent + ): Promise { + await this.updateState('response', event, AuthorizationResponseStateStatus.RECEIVED) + } + + private async onAuthorizationResponseReceivedFailed(event: AuthorizationEvent): Promise { + await this.updateState('response', event, AuthorizationResponseStateStatus.ERROR) + } + + private async onAuthorizationResponseVerifiedFailed(event: AuthorizationEvent): Promise { + await this.updateState('response', event, AuthorizationResponseStateStatus.ERROR) + } + + private async onAuthorizationResponseVerifiedSuccess( + event: AuthorizationEvent + ): Promise { + await this.updateState('response', event, AuthorizationResponseStateStatus.VERIFIED) + } + + public async getCorrelationIdByNonce(nonce: string, errorOnNotFound?: boolean): Promise { + return await this.getCorrelationIdImpl('nonce', nonce, errorOnNotFound) + } + + public async getCorrelationIdByState(state: string, errorOnNotFound?: boolean): Promise { + return await this.getCorrelationIdImpl('state', state, errorOnNotFound) + } + + private async getCorrelationIdImpl( + type: 'nonce' | 'state', + key: string, + errorOnNotFound?: boolean + ): Promise { + let correlationId: string + if (type === 'nonce') { + correlationId = this.nonceToCorrelationId[key] + } else if (type === 'state') { + correlationId = this.stateToCorrelationId[key] + } else { + throw new AriesFrameworkError(`Unknown type ${type}`) + } + + if (!correlationId && errorOnNotFound) throw new AriesFrameworkError(`Could not find ${type} '${key}'`) + + return correlationId + } + + private async updateMapping( + mapping: Record, + event: AuthorizationEvent, + propertyKey: string, + value: T, + allowExisting: boolean + ) { + const key = (await event.subject.getMergedProperty(propertyKey)) as string + if (!key) { + throw new AriesFrameworkError(`No value found for key ${value} in Authorization Request`) + } + + const existing = mapping[key] + + if (existing) { + if (!allowExisting) { + throw new AriesFrameworkError(`Mapping exists for key ${propertyKey} and we do not allow overwriting values`) + } else if (existing !== value) { + throw new AriesFrameworkError('Value changed for key') + } + } + if (!value) { + delete mapping[key] + } else { + mapping[key] = value + } + } + + private async updateState( + type: 'request' | 'response', + event: AuthorizationEvent, + status: AuthorizationRequestStateStatus | AuthorizationResponseStateStatus + ): Promise { + if (!event.correlationId) { + throw new AriesFrameworkError(`'${type} ${status}' event without correlation id received`) + } + + try { + const eventState = { + correlationId: event.correlationId, + ...(type === 'request' ? { request: event.subject } : {}), + ...(type === 'response' ? { response: event.subject } : {}), + ...(event.error ? { error: event.error } : {}), + status, + timestamp: event.timestamp, + lastUpdated: event.timestamp, + } + if (type === 'request') { + this.authorizationRequests[event.correlationId] = eventState as AuthorizationRequestState + // We do not await these + this.updateMapping(this.nonceToCorrelationId, event, 'nonce', event.correlationId, true).catch((error) => + this.logger.error(JSON.stringify(error)) + ) + this.nonceToCorrelationId + this.updateMapping(this.stateToCorrelationId, event, 'state', event.correlationId, true).catch((error) => + this.logger.error(JSON.stringify(error)) + ) + } else { + this.authorizationResponses[event.correlationId] = eventState as AuthorizationResponseState + } + } catch (error: unknown) { + this.logger.error(`Error in update state happened: ${error}`) + } + } + + private static async cleanMappingForCorrelationId( + mapping: Record, + correlationId: string + ): Promise { + const keys = InMemoryVerifierSessionManager.getKeysForCorrelationId(mapping, correlationId) + if (keys && keys.length > 0) { + keys.forEach((key) => delete mapping[key]) + } + } + + public async deleteStateForCorrelationId(correlationId: string) { + InMemoryVerifierSessionManager.cleanMappingForCorrelationId(this.nonceToCorrelationId, correlationId).catch( + (error) => this.logger.error(JSON.stringify(error)) + ) + InMemoryVerifierSessionManager.cleanMappingForCorrelationId(this.stateToCorrelationId, correlationId).catch( + (error) => this.logger.error(JSON.stringify(error)) + ) + delete this.authorizationRequests[correlationId] + delete this.authorizationResponses[correlationId] + delete this.correlationIdToVerifyProofResponseOptions[correlationId] + } + + private async cleanup() { + const now = Date.now() + const maxAgeInMS = this.maxAgeInSeconds * 1000 + + const cleanupCorrelations = async ( + reqByCorrelationId: [string, AuthorizationRequestState | AuthorizationResponseState] + ) => { + const correlationId = reqByCorrelationId[0] + const authState = reqByCorrelationId[1] + + const ts = authState.lastUpdated || authState.timestamp + if (maxAgeInMS !== 0 && now > ts + maxAgeInMS) { + await this.deleteStateForCorrelationId(correlationId) + } + } + + const authRequests = Object.entries(this.authorizationRequests).map(cleanupCorrelations) + const authResponses = Object.entries(this.authorizationResponses).map(cleanupCorrelations) + await Promise.all([...authRequests, ...authResponses]) + } +} diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts index 95b524dc68..9aa9d05683 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts @@ -50,7 +50,7 @@ export class OpenId4VcVerifierApi { * @param options.proofRequestMetadata - Metadata about the proof request. * @returns @see VerifiedProofResponse object containing the idTokenPayload and the verified submission. */ - public async verifyProofResponse(proofPayload: ProofPayload, options: VerifyProofResponseOptions) { - return await this.openId4VcVerifierService.verifyProofResponse(this.agentContext, proofPayload, options) + public async verifyProofResponse(proofPayload: ProofPayload) { + return await this.openId4VcVerifierService.verifyProofResponse(this.agentContext, proofPayload) } } diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierModule.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierModule.ts index 38b68df2ea..5485787c60 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierModule.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierModule.ts @@ -1,8 +1,12 @@ -import type { DependencyManager, Module } from '@aries-framework/core' +import type { OpenId4VcVerifierModuleConfigOptions } from './OpenId4VcVerifierModuleConfig' +import type { AgentContext, DependencyManager, Module } from '@aries-framework/core' +import type { AuthorizationResponsePayload } from '@sphereon/did-auth-siop' import { AgentConfig } from '@aries-framework/core' +import bodyParser from 'body-parser' import { OpenId4VcVerifierApi } from './OpenId4VcVerifierApi' +import { OpenId4VcVerifierModuleConfig } from './OpenId4VcVerifierModuleConfig' import { OpenId4VcVerifierService } from './OpenId4VcVerifierService' /** @@ -11,6 +15,12 @@ import { OpenId4VcVerifierService } from './OpenId4VcVerifierService' export class OpenId4VcVerifierModule implements Module { public readonly api = OpenId4VcVerifierApi + public readonly config: OpenId4VcVerifierModuleConfig + + public constructor(options: OpenId4VcVerifierModuleConfigOptions) { + this.config = new OpenId4VcVerifierModuleConfig(options) + } + /** * Registers the dependencies of the question answer module on the dependency manager. */ @@ -22,10 +32,42 @@ export class OpenId4VcVerifierModule implements Module { "The '@aries-framework/openid4vc-verifier' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." ) + // Register config + dependencyManager.registerInstance(OpenId4VcVerifierModuleConfig, this.config) + // Api dependencyManager.registerContextScoped(OpenId4VcVerifierApi) // Services dependencyManager.registerSingleton(OpenId4VcVerifierService) } + + public async initialize(agentContext: AgentContext): Promise { + if (!this.config.endPointConfig) return + + // create application/x-www-form-urlencoded parser + const urlencodedParser = bodyParser.urlencoded({ extended: false }) + + const endPointConfig = this.config.endPointConfig + + endPointConfig.app.post(endPointConfig.verificationEndpointPath, urlencodedParser, async (req, res, next) => { + try { + const isVpRequest = req.body.presentation_submission !== undefined + const verifierService = await agentContext.dependencyManager.resolve(OpenId4VcVerifierService) + + const authorizationResponse: AuthorizationResponsePayload = req.body + if (isVpRequest) authorizationResponse.presentation_submission = JSON.parse(req.body.presentation_submission) + + const verifiedProofResponse = await verifierService.verifyProofResponse(agentContext, req.body) + if (!endPointConfig.proofResponseHandler) return res.status(200).send() + + const { status } = await endPointConfig.proofResponseHandler(verifiedProofResponse) + return res.status(status).send() + } catch (error: unknown) { + next(error) + } + + return res.status(200).send() + }) + } } diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierModuleConfig.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierModuleConfig.ts new file mode 100644 index 0000000000..1f2aac2685 --- /dev/null +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierModuleConfig.ts @@ -0,0 +1,32 @@ +import type { IInMemoryVerifierSessionManager } from './InMemoryVerifierSessionManager' +import type { VerifiedProofResponse } from './OpenId4VcVerifierServiceOptions' +import type { Express } from 'express' + +export type ProofResponseHandlerReturn = { status: number } +export type ProofResponseHandler = (verifiedProofResponse: VerifiedProofResponse) => Promise + +export interface EndPointConfig { + app: Express + verificationEndpointPath: string + proofResponseHandler?: ProofResponseHandler +} +export interface OpenId4VcVerifierModuleConfigOptions { + SessionManager?: IInMemoryVerifierSessionManager + endPointConfig?: EndPointConfig +} + +export class OpenId4VcVerifierModuleConfig { + private options: OpenId4VcVerifierModuleConfigOptions + + public constructor(options: OpenId4VcVerifierModuleConfigOptions) { + this.options = options + } + + public get endPointConfig() { + return this.options.endPointConfig + } + + public get sessionManager() { + return this.options.SessionManager + } +} diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts index 3bff2cdbb9..bb59c62045 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts @@ -1,5 +1,5 @@ +import type { IInMemoryVerifierSessionManager } from './InMemoryVerifierSessionManager' import type { - VerifyProofResponseOptions, ProofRequestWithMetadata, CreateProofRequestOptions, ProofRequestMetadata, @@ -9,7 +9,6 @@ import type { AgentContext, W3cVerifyPresentationResult } from '@aries-framework import type { AuthorizationResponsePayload, ClientMetadataOpts, - PresentationDefinitionWithLocation, PresentationVerificationCallback, SigningAlgo, } from '@sphereon/did-auth-siop' @@ -36,8 +35,12 @@ import { PresentationDefinitionLocation, PassBy, VerificationMode, + AuthorizationResponse, } from '@sphereon/did-auth-siop' +import { EventEmitter } from 'events' +import { InMemoryVerifierSessionManager } from './InMemoryVerifierSessionManager' +import { OpenId4VcVerifierModuleConfig } from './OpenId4VcVerifierModuleConfig' import { staticOpOpenIdConfig, staticOpSiopConfig } from './OpenId4VcVerifierServiceOptions' import { getSupportedDidMethods, @@ -53,10 +56,21 @@ import { export class OpenId4VcVerifierService { private logger: Logger private w3cCredentialService: W3cCredentialService - - public constructor(@inject(InjectionSymbols.Logger) logger: Logger, w3cCredentialService: W3cCredentialService) { + private openId4VcVerifierModuleConfig: OpenId4VcVerifierModuleConfig + private sessionManager: IInMemoryVerifierSessionManager + private eventEmitter: EventEmitter + + public constructor( + @inject(InjectionSymbols.Logger) logger: Logger, + w3cCredentialService: W3cCredentialService, + openId4VcVerifierModuleConfig: OpenId4VcVerifierModuleConfig + ) { this.w3cCredentialService = w3cCredentialService this.logger = logger + this.openId4VcVerifierModuleConfig = openId4VcVerifierModuleConfig + this.eventEmitter = new EventEmitter() + this.sessionManager = + openId4VcVerifierModuleConfig.sessionManager ?? new InMemoryVerifierSessionManager(this.eventEmitter, logger) } public async getRelyingParty( @@ -147,9 +161,9 @@ export class OpenId4VcVerifierService { .withAuthorizationEndpoint(authorizationEndpoint) .withCheckLinkedDomain(CheckLinkedDomain.NEVER) .withRevocationVerification(RevocationVerification.NEVER) + .withSessionManager(this.sessionManager) + .withEventEmitter(this.eventEmitter) // .withWellknownDIDVerifyCallback - // .withEventEmitter - // .withSessionManager // For now we use no session manager if (proofRequestMetadata) { builder.withPresentationVerification( @@ -190,43 +204,68 @@ export class OpenId4VcVerifierService { const authorizationRequestUri = await authorizationRequest.uri() const encodedAuthorizationRequestUri = authorizationRequestUri.encodedUri + const proofRequestMetadata = { correlationId, challenge, state } + + await this.sessionManager.saveVerifyProofResponseOptions(correlationId, { + createProofRequestOptions: options, + proofRequestMetadata, + }) + return { proofRequest: encodedAuthorizationRequestUri, - proofRequestMetadata: { - correlationId, - challenge, - state, - }, + proofRequestMetadata, } } public async verifyProofResponse( agentContext: AgentContext, - authorizationResponsePayload: AuthorizationResponsePayload, - options: VerifyProofResponseOptions + authorizationResponsePayload: AuthorizationResponsePayload ): Promise { - const { createProofRequestOptions, proofRequestMetadata } = options - const { state, challenge, correlationId } = proofRequestMetadata + let authorizationResponse: AuthorizationResponse + try { + authorizationResponse = await AuthorizationResponse.fromPayload(authorizationResponsePayload) + } catch (error: unknown) { + throw new AriesFrameworkError( + `Unable to parse authorization response payload. ${JSON.stringify(authorizationResponsePayload)}` + ) + } - const relyingParty = await this.getRelyingParty(agentContext, createProofRequestOptions, proofRequestMetadata) + let correlationId: string | undefined + const resNonce = (await authorizationResponse.getMergedProperty('nonce', false)) as string + const resState = (await authorizationResponse.getMergedProperty('state', false)) as string + correlationId = await this.sessionManager.getCorrelationIdByNonce(resNonce, false) + if (!correlationId) { + correlationId = await this.sessionManager.getCorrelationIdByState(resState, false) + } - const presentationDefinition = createProofRequestOptions.presentationDefinition + if (!correlationId) { + throw new AriesFrameworkError(`Unable to find correlationId for nonce '${resNonce}' or state '${resState}'`) + } + const result = await this.sessionManager.getVerifiyProofResponseOptions(correlationId) - let presentationDefinitionsWithLocation: [PresentationDefinitionWithLocation] | undefined - if (presentationDefinition) { - presentationDefinitionsWithLocation = [ - { - definition: presentationDefinition, - location: PresentationDefinitionLocation.CLAIMS_VP_TOKEN, // For now we always use the VP_TOKEN - }, - ] + if (!result) { + throw new AriesFrameworkError(`Unable to associate a request to the response correlationId '${correlationId}'`) } + const { createProofRequestOptions, proofRequestMetadata } = result + const presentationDefinition = createProofRequestOptions.presentationDefinition + + const presentationDefinitionsWithLocation = presentationDefinition + ? [ + { + definition: presentationDefinition, + location: PresentationDefinitionLocation.CLAIMS_VP_TOKEN, // For now we always use the VP_TOKEN + }, + ] + : undefined + + const relyingParty = await this.getRelyingParty(agentContext, createProofRequestOptions, proofRequestMetadata) + const response = await relyingParty.verifyAuthorizationResponse(authorizationResponsePayload, { audience: createProofRequestOptions.verificationMethod.id, correlationId, - nonce: challenge, - state, + nonce: proofRequestMetadata.challenge, + state: proofRequestMetadata.state, presentationDefinitions: presentationDefinitionsWithLocation, verification: { mode: VerificationMode.INTERNAL, diff --git a/packages/openid4vc-verifier/src/index.ts b/packages/openid4vc-verifier/src/index.ts index 0eaceb2542..56a1c21dfa 100644 --- a/packages/openid4vc-verifier/src/index.ts +++ b/packages/openid4vc-verifier/src/index.ts @@ -1,4 +1,12 @@ +import * as Presentation from './presentation' + +export { Presentation } + +export { OpenId4VpHolderService, PresentationExchangeService } from './presentation' + export * from './OpenId4VcVerifierApi' export * from './OpenId4VcVerifierModule' export * from './OpenId4VcVerifierService' export * from './OpenId4VcVerifierServiceOptions' +export * from './OpenId4VcVerifierModuleConfig' +export * from './InMemoryVerifierSessionManager' diff --git a/packages/openid4vc-holder/src/presentation/OpenId4VpHolderService.ts b/packages/openid4vc-verifier/src/presentation/OpenId4VpHolderService.ts similarity index 100% rename from packages/openid4vc-holder/src/presentation/OpenId4VpHolderService.ts rename to packages/openid4vc-verifier/src/presentation/OpenId4VpHolderService.ts diff --git a/packages/openid4vc-holder/src/presentation/OpenId4VpHolderServiceOptions.ts b/packages/openid4vc-verifier/src/presentation/OpenId4VpHolderServiceOptions.ts similarity index 100% rename from packages/openid4vc-holder/src/presentation/OpenId4VpHolderServiceOptions.ts rename to packages/openid4vc-verifier/src/presentation/OpenId4VpHolderServiceOptions.ts diff --git a/packages/openid4vc-holder/src/presentation/PresentationExchangeService.ts b/packages/openid4vc-verifier/src/presentation/PresentationExchangeService.ts similarity index 100% rename from packages/openid4vc-holder/src/presentation/PresentationExchangeService.ts rename to packages/openid4vc-verifier/src/presentation/PresentationExchangeService.ts diff --git a/packages/openid4vc-holder/src/presentation/index.ts b/packages/openid4vc-verifier/src/presentation/index.ts similarity index 100% rename from packages/openid4vc-holder/src/presentation/index.ts rename to packages/openid4vc-verifier/src/presentation/index.ts diff --git a/packages/openid4vc-holder/src/presentation/selection/PexCredentialSelection.ts b/packages/openid4vc-verifier/src/presentation/selection/PexCredentialSelection.ts similarity index 100% rename from packages/openid4vc-holder/src/presentation/selection/PexCredentialSelection.ts rename to packages/openid4vc-verifier/src/presentation/selection/PexCredentialSelection.ts diff --git a/packages/openid4vc-holder/src/presentation/selection/example.md b/packages/openid4vc-verifier/src/presentation/selection/example.md similarity index 100% rename from packages/openid4vc-holder/src/presentation/selection/example.md rename to packages/openid4vc-verifier/src/presentation/selection/example.md diff --git a/packages/openid4vc-holder/src/presentation/selection/index.ts b/packages/openid4vc-verifier/src/presentation/selection/index.ts similarity index 100% rename from packages/openid4vc-holder/src/presentation/selection/index.ts rename to packages/openid4vc-verifier/src/presentation/selection/index.ts diff --git a/packages/openid4vc-holder/src/presentation/selection/types.ts b/packages/openid4vc-verifier/src/presentation/selection/types.ts similarity index 100% rename from packages/openid4vc-holder/src/presentation/selection/types.ts rename to packages/openid4vc-verifier/src/presentation/selection/types.ts diff --git a/packages/openid4vc-holder/src/presentation/transform.ts b/packages/openid4vc-verifier/src/presentation/transform.ts similarity index 100% rename from packages/openid4vc-holder/src/presentation/transform.ts rename to packages/openid4vc-verifier/src/presentation/transform.ts diff --git a/packages/openid4vc-verifier/tests/openId4vc-verifier-module.test.ts b/packages/openid4vc-verifier/tests/openId4vc-verifier-module.test.ts index a1cd46065c..ca9f1b99dd 100644 --- a/packages/openid4vc-verifier/tests/openId4vc-verifier-module.test.ts +++ b/packages/openid4vc-verifier/tests/openId4vc-verifier-module.test.ts @@ -14,9 +14,11 @@ const dependencyManager = { describe('OpenId4VcIssuerModule', () => { test('registers dependencies on the dependency manager', () => { - const openId4VcClientModule = new OpenId4VcVerifierModule() + const openId4VcClientModule = new OpenId4VcVerifierModule({}) openId4VcClientModule.register(dependencyManager) + expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerContextScoped).toHaveBeenCalledTimes(1) expect(dependencyManager.registerContextScoped).toHaveBeenCalledWith(OpenId4VcVerifierApi) diff --git a/packages/openid4vc-verifier/tests/openid4vc-verifier.e2e.test.ts b/packages/openid4vc-verifier/tests/openid4vc-verifier.e2e.test.ts index d3f45bdaae..8c757750f1 100644 --- a/packages/openid4vc-verifier/tests/openid4vc-verifier.e2e.test.ts +++ b/packages/openid4vc-verifier/tests/openid4vc-verifier.e2e.test.ts @@ -11,7 +11,7 @@ import { cleanAll, enableNetConnect } from 'nock' import { OpenId4VcVerifierModule, staticOpOpenIdConfig, staticOpSiopConfig } from '../src' const modules = { - openId4VcVerifier: new OpenId4VcVerifierModule(), + openId4VcVerifier: new OpenId4VcVerifierModule({}), askar: new AskarModule({ ariesAskar, }), diff --git a/yarn.lock b/yarn.lock index b9f8c4a62f..eb3ac7833f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2925,6 +2925,14 @@ "@types/connect" "*" "@types/node" "*" +"@types/body-parser@^1.19.5": + version "1.19.5" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + dependencies: + "@types/connect" "*" + "@types/node" "*" + "@types/connect@*": version "3.4.35" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" @@ -2976,6 +2984,16 @@ "@types/qs" "*" "@types/serve-static" "*" +"@types/express@^4.17.21": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + "@types/figlet@^1.5.4": version "1.5.5" resolved "https://registry.yarnpkg.com/@types/figlet/-/figlet-1.5.5.tgz#da93169178f0187da288c313ab98ab02fb1e8b8c" @@ -3040,10 +3058,10 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== -"@types/jsonpath@^0.2.1": - version "0.2.1" - resolved "https://registry.yarnpkg.com/@types/jsonpath/-/jsonpath-0.2.1.tgz#92a5f0328a58848449dd52249cbba270364e82e5" - integrity sha512-CmRqkJfGIthwvW6vbNeY8wI3opKqnvX8+ec83PcK14Ee3RSla1ErAFeY/gVsh42Dm/uLCnD+pkQEDDkKuBK2bQ== +"@types/jsonpath@^0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@types/jsonpath/-/jsonpath-0.2.4.tgz#065be59981c1420832835af656377622271154be" + integrity sha512-K3hxB8Blw0qgW6ExKgMbXQv2UPZBoE2GqLpVY+yr7nMD2Pq86lsuIzyAaiQ7eMqFL5B6di6pxSkogLJEyEHoGA== "@types/long@^4.0.1": version "4.0.2" @@ -4022,6 +4040,24 @@ body-parser@1.20.1: type-is "~1.6.18" unpipe "1.0.0" +body-parser@^1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + borc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/borc/-/borc-3.0.0.tgz#49ada1be84de86f57bb1bb89789f34c186dfa4fe" @@ -4696,7 +4732,7 @@ content-disposition@0.5.4: dependencies: safe-buffer "5.2.1" -content-type@~1.0.4: +content-type@~1.0.4, content-type@~1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== @@ -5842,7 +5878,7 @@ expo-random@*: dependencies: base64-js "^1.3.0" -express@^4.17.1: +express@^4.17.1, express@^4.18.2: version "4.18.2" resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== @@ -10742,6 +10778,16 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + rc@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -12488,16 +12534,16 @@ unbox-primitive@^1.0.2: has-symbols "^1.0.3" which-boxed-primitive "^1.0.2" -undici-types@~5.26.4: - version "5.26.5" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" - integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== - underscore@1.12.1: version "1.12.1" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.12.1.tgz#7bb8cc9b3d397e201cf8553336d262544ead829e" integrity sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw== +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + unicode-canonical-property-names-ecmascript@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" From f450617dd14da85f9996208b20b634b5ec22c280 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 22 Nov 2023 10:33:35 +0100 Subject: [PATCH 069/115] feat: restructure issuance side --- packages/openid4vc-holder/package.json | 3 +-- .../src/OpenId4VcHolderApi.ts | 5 ++--- .../src/OpenId4VcHolderModule.ts | 2 +- packages/openid4vc-holder/src/issuance.ts | 19 +++++++++++++++++++ .../tests/openId4vc-holder-module.test.ts | 8 ++++---- .../tests/openid4vci-holder.e2e.test.ts | 2 +- packages/openid4vc-issuer/package.json | 1 + packages/openid4vc-issuer/src/index.ts | 6 ++++++ .../src/issuance/OpenId4VciHolderService.ts | 0 .../OpenId4VciHolderServiceOptions.ts | 0 .../src/issuance/index.ts | 0 .../src/issuance/utils/Formats.ts | 0 .../src/issuance/utils/IssuerMetadataUtils.ts | 0 .../__tests__/claimFormatMapping.test.ts | 0 .../src/issuance/utils/claimFormatMapping.ts | 0 .../src/issuance/utils/index.ts | 0 .../src/OpenId4VcVerifierApi.ts | 6 +----- 17 files changed, 36 insertions(+), 16 deletions(-) create mode 100644 packages/openid4vc-holder/src/issuance.ts rename packages/{openid4vc-holder => openid4vc-issuer}/src/issuance/OpenId4VciHolderService.ts (100%) rename packages/{openid4vc-holder => openid4vc-issuer}/src/issuance/OpenId4VciHolderServiceOptions.ts (100%) rename packages/{openid4vc-holder => openid4vc-issuer}/src/issuance/index.ts (100%) rename packages/{openid4vc-holder => openid4vc-issuer}/src/issuance/utils/Formats.ts (100%) rename packages/{openid4vc-holder => openid4vc-issuer}/src/issuance/utils/IssuerMetadataUtils.ts (100%) rename packages/{openid4vc-holder => openid4vc-issuer}/src/issuance/utils/__tests__/claimFormatMapping.test.ts (100%) rename packages/{openid4vc-holder => openid4vc-issuer}/src/issuance/utils/claimFormatMapping.ts (100%) rename packages/{openid4vc-holder => openid4vc-issuer}/src/issuance/utils/index.ts (100%) diff --git a/packages/openid4vc-holder/package.json b/packages/openid4vc-holder/package.json index 808e54999b..20cc19d8cd 100644 --- a/packages/openid4vc-holder/package.json +++ b/packages/openid4vc-holder/package.json @@ -27,8 +27,7 @@ "@aries-framework/askar": "^0.4.2", "@aries-framework/core": "0.4.2", "@aries-framework/openid4vc-verifier": "0.4.2", - "@sphereon/oid4vci-client": "^0.8.1", - "@sphereon/oid4vci-common": "^0.8.1" + "@aries-framework/openid4vc-issuer": "0.4.2" }, "devDependencies": { "@aries-framework/node": "^0.4.2", diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts index e76cb0c8b5..108ebcb006 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts @@ -4,15 +4,14 @@ import type { AuthCodeFlowOptions, AcceptCredentialOfferOptions, CredentialOfferPayloadV1_0_11, -} from './issuance/OpenId4VciHolderServiceOptions' +} from './issuance' import type { AuthenticationRequest, PresentationRequest, PresentationSubmission } from './presentation' import type { VerificationMethod, W3cCredentialRecord } from '@aries-framework/core' import { injectable, AgentContext } from '@aries-framework/core' +import { OpenId4VciHolderService } from '@aries-framework/openid4vc-issuer' import { OpenId4VpHolderService } from '@aries-framework/openid4vc-verifier' -import { OpenId4VciHolderService } from './issuance/OpenId4VciHolderService' - /** * @public */ diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts b/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts index e50031cb87..fe63de5d06 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts @@ -1,10 +1,10 @@ import type { DependencyManager, Module } from '@aries-framework/core' import { AgentConfig } from '@aries-framework/core' +import { OpenId4VciHolderService } from '@aries-framework/openid4vc-issuer' import { OpenId4VpHolderService, PresentationExchangeService } from '@aries-framework/openid4vc-verifier' import { OpenId4VcHolderApi } from './OpenId4VcHolderApi' -import { OpenId4VciHolderService } from './issuance/OpenId4VciHolderService' /** * @public @module OpenId4VcHolderModule diff --git a/packages/openid4vc-holder/src/issuance.ts b/packages/openid4vc-holder/src/issuance.ts new file mode 100644 index 0000000000..61ae53cfd9 --- /dev/null +++ b/packages/openid4vc-holder/src/issuance.ts @@ -0,0 +1,19 @@ +import type { Issuance } from '@aries-framework/openid4vc-issuer' + +export type AcceptCredentialOfferOptions = Issuance.AcceptCredentialOfferOptions +export type AuthCodeFlowOptions = Issuance.AuthCodeFlowOptions +export type AuthDetails = Issuance.AuthDetails +export type CredentialOfferPayloadV1_0_11 = Issuance.CredentialOfferPayloadV1_0_11 +export type CredentialToRequest = Issuance.CredentialToRequest +export type EndpointMetadataResult = Issuance.EndpointMetadataResult +export type OfferedCredentialType = Issuance.OfferedCredentialType +export type OpenId4VCIVersion = Issuance.OpenId4VCIVersion +export type OpenIdCredentialFormatProfile = Issuance.OpenIdCredentialFormatProfile +export type ProofOfPossessionRequirements = Issuance.ProofOfPossessionRequirements +export type ProofOfPossessionVerificationMethodResolver = Issuance.ProofOfPossessionVerificationMethodResolver +export type ProofOfPossessionVerificationMethodResolverOptions = + Issuance.ProofOfPossessionVerificationMethodResolverOptions +export type ResolvedAuthorizationRequest = Issuance.ResolvedAuthorizationRequest +export type ResolvedAuthorizationRequestWithCode = Issuance.ResolvedAuthorizationRequestWithCode +export type ResolvedCredentialOffer = Issuance.ResolvedCredentialOffer +export type SupportedCredentialFormats = Issuance.SupportedCredentialFormats diff --git a/packages/openid4vc-holder/tests/openId4vc-holder-module.test.ts b/packages/openid4vc-holder/tests/openId4vc-holder-module.test.ts index 014bc09c80..7d70b29c8a 100644 --- a/packages/openid4vc-holder/tests/openId4vc-holder-module.test.ts +++ b/packages/openid4vc-holder/tests/openId4vc-holder-module.test.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/unbound-method */ import type { DependencyManager } from '@aries-framework/core' -import { Presentation } from '@aries-framework/openid4vc-verifier' +import { OpenId4VciHolderService } from '@aries-framework/openid4vc-issuer' +import { OpenId4VpHolderService, PresentationExchangeService } from '@aries-framework/openid4vc-verifier' import { OpenId4VcHolderApi } from '../src/OpenId4VcHolderApi' import { OpenId4VcHolderModule } from '../src/OpenId4VcHolderModule' -import { OpenId4VciHolderService } from '../src/issuance/OpenId4VciHolderService' const dependencyManager = { registerInstance: jest.fn(), @@ -24,7 +24,7 @@ describe('OpenId4VcHolderModule', () => { expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(3) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VciHolderService) - expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(Presentation.OpenId4VpHolderService) - expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(Presentation.PresentationExchangeService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VpHolderService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(PresentationExchangeService) }) }) diff --git a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts index 93a2830c0b..7f9329d622 100644 --- a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts @@ -14,7 +14,7 @@ import { agentDependencies } from '@aries-framework/node' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import nock, { cleanAll, enableNetConnect } from 'nock' -import { OpenIdCredentialFormatProfile } from '../src' +import { OpenIdCredentialFormatProfile } from '../' import { OpenId4VcHolderModule } from '../src/OpenId4VcHolderModule' import { diff --git a/packages/openid4vc-issuer/package.json b/packages/openid4vc-issuer/package.json index e5717d585c..648f094239 100644 --- a/packages/openid4vc-issuer/package.json +++ b/packages/openid4vc-issuer/package.json @@ -28,6 +28,7 @@ "@aries-framework/core": "0.4.2", "@sphereon/oid4vci-issuer": "^0.8.1", "@sphereon/oid4vci-common": "^0.8.1", + "@sphereon/oid4vci-client": "^0.8.1", "@sphereon/ssi-types": "^0.17.5" }, "devDependencies": { diff --git a/packages/openid4vc-issuer/src/index.ts b/packages/openid4vc-issuer/src/index.ts index f99285d628..18b7cbfcf1 100644 --- a/packages/openid4vc-issuer/src/index.ts +++ b/packages/openid4vc-issuer/src/index.ts @@ -1,3 +1,9 @@ +import * as Issuance from './issuance' + +export { Issuance } + +export { OpenId4VciHolderService } from './issuance' + export * from './OpenId4VcIssuerApi' export * from './OpenId4VcIssuerModule' export * from './OpenId4VcIssuerService' diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts b/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderService.ts similarity index 100% rename from packages/openid4vc-holder/src/issuance/OpenId4VciHolderService.ts rename to packages/openid4vc-issuer/src/issuance/OpenId4VciHolderService.ts diff --git a/packages/openid4vc-holder/src/issuance/OpenId4VciHolderServiceOptions.ts b/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderServiceOptions.ts similarity index 100% rename from packages/openid4vc-holder/src/issuance/OpenId4VciHolderServiceOptions.ts rename to packages/openid4vc-issuer/src/issuance/OpenId4VciHolderServiceOptions.ts diff --git a/packages/openid4vc-holder/src/issuance/index.ts b/packages/openid4vc-issuer/src/issuance/index.ts similarity index 100% rename from packages/openid4vc-holder/src/issuance/index.ts rename to packages/openid4vc-issuer/src/issuance/index.ts diff --git a/packages/openid4vc-holder/src/issuance/utils/Formats.ts b/packages/openid4vc-issuer/src/issuance/utils/Formats.ts similarity index 100% rename from packages/openid4vc-holder/src/issuance/utils/Formats.ts rename to packages/openid4vc-issuer/src/issuance/utils/Formats.ts diff --git a/packages/openid4vc-holder/src/issuance/utils/IssuerMetadataUtils.ts b/packages/openid4vc-issuer/src/issuance/utils/IssuerMetadataUtils.ts similarity index 100% rename from packages/openid4vc-holder/src/issuance/utils/IssuerMetadataUtils.ts rename to packages/openid4vc-issuer/src/issuance/utils/IssuerMetadataUtils.ts diff --git a/packages/openid4vc-holder/src/issuance/utils/__tests__/claimFormatMapping.test.ts b/packages/openid4vc-issuer/src/issuance/utils/__tests__/claimFormatMapping.test.ts similarity index 100% rename from packages/openid4vc-holder/src/issuance/utils/__tests__/claimFormatMapping.test.ts rename to packages/openid4vc-issuer/src/issuance/utils/__tests__/claimFormatMapping.test.ts diff --git a/packages/openid4vc-holder/src/issuance/utils/claimFormatMapping.ts b/packages/openid4vc-issuer/src/issuance/utils/claimFormatMapping.ts similarity index 100% rename from packages/openid4vc-holder/src/issuance/utils/claimFormatMapping.ts rename to packages/openid4vc-issuer/src/issuance/utils/claimFormatMapping.ts diff --git a/packages/openid4vc-holder/src/issuance/utils/index.ts b/packages/openid4vc-issuer/src/issuance/utils/index.ts similarity index 100% rename from packages/openid4vc-holder/src/issuance/utils/index.ts rename to packages/openid4vc-issuer/src/issuance/utils/index.ts diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts index 9aa9d05683..fd79e3c150 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts @@ -1,8 +1,4 @@ -import type { - CreateProofRequestOptions, - ProofPayload, - VerifyProofResponseOptions, -} from './OpenId4VcVerifierServiceOptions' +import type { CreateProofRequestOptions, ProofPayload } from './OpenId4VcVerifierServiceOptions' import { injectable, AgentContext } from '@aries-framework/core' From 9113461fb9ded9932ae36859951c9a14eef5a06c Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 22 Nov 2023 11:10:13 +0100 Subject: [PATCH 070/115] refactor: rename file --- packages/openid4vc-verifier/src/{shared.ts => utils.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/openid4vc-verifier/src/{shared.ts => utils.ts} (100%) diff --git a/packages/openid4vc-verifier/src/shared.ts b/packages/openid4vc-verifier/src/utils.ts similarity index 100% rename from packages/openid4vc-verifier/src/shared.ts rename to packages/openid4vc-verifier/src/utils.ts From ee031ab57a30233f4ac1f9ad6d22b5adbe587edf Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 22 Nov 2023 11:10:23 +0100 Subject: [PATCH 071/115] fix: remove duplicate --- .../openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts | 8 +++----- .../src/issuance/OpenId4VciHolderServiceOptions.ts | 5 +---- .../openid4vc-verifier/src/OpenId4VcVerifierService.ts | 2 +- .../src/presentation/OpenId4VpHolderService.ts | 2 +- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts index 4be06292a1..c620b670e5 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts @@ -1,3 +1,4 @@ +import type { SupportedCredentialFormats } from './issuance' import type { VerificationMethod, W3cCredential } from '@aries-framework/core' import type { CommonCredentialSupported, @@ -9,16 +10,13 @@ import type { export type { MetadataDisplay, ProofOfPossession, CredentialOfferPayloadV1_0_11 } -// TODO: duplicate -export type CredentialFormatSupported = 'jwt_vc_json' | 'jwt_vc_json-ld' - export interface CredentialOfferFormat { - format: CredentialFormatSupported + format: SupportedCredentialFormats types: string[] } export interface CredentialSupported extends CommonCredentialSupported { - format: CredentialFormatSupported + format: SupportedCredentialFormats types: string[] } diff --git a/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderServiceOptions.ts b/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderServiceOptions.ts index 10c345a257..81c32353a9 100644 --- a/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderServiceOptions.ts +++ b/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderServiceOptions.ts @@ -1,11 +1,8 @@ import type { OfferedCredentialType } from './utils/IssuerMetadataUtils' -import type { OpenIdCredentialFormatProfile } from './utils/claimFormatMapping' import type { JwaSignatureAlgorithm, KeyType, VerificationMethod } from '@aries-framework/core' import type { CredentialOfferPayloadV1_0_11, EndpointMetadataResult, OpenId4VCIVersion } from '@sphereon/oid4vci-common' -export type SupportedCredentialFormats = - | OpenIdCredentialFormatProfile.JwtVcJson - | OpenIdCredentialFormatProfile.JwtVcJsonLd +export type SupportedCredentialFormats = 'jwt_vc_json' | 'jwt_vc_json-ld' export type { OfferedCredentialType, OpenId4VCIVersion, EndpointMetadataResult, CredentialOfferPayloadV1_0_11 } diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts index bb59c62045..bee10828c6 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts @@ -47,7 +47,7 @@ import { getSuppliedSignatureFromVerificationMethod, getResolver, getSupportedJwaSignatureAlgorithms, -} from './shared' +} from './utils' /** * @internal diff --git a/packages/openid4vc-verifier/src/presentation/OpenId4VpHolderService.ts b/packages/openid4vc-verifier/src/presentation/OpenId4VpHolderService.ts index 32bad0f8f5..9d374fe031 100644 --- a/packages/openid4vc-verifier/src/presentation/OpenId4VpHolderService.ts +++ b/packages/openid4vc-verifier/src/presentation/OpenId4VpHolderService.ts @@ -31,7 +31,7 @@ import { VerificationMode, } from '@sphereon/did-auth-siop' -import { getResolver, getSuppliedSignatureFromVerificationMethod, getSupportedDidMethods } from '../shared' +import { getResolver, getSuppliedSignatureFromVerificationMethod, getSupportedDidMethods } from '../utils' import { PresentationExchangeService } from './PresentationExchangeService' From 903ecae05c4ee2d05ce05e2cc3fa58383be5a0e1 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Wed, 22 Nov 2023 12:00:32 +0100 Subject: [PATCH 072/115] refactor: rename types and variables --- .../src/OpenId4VcVerifierModule.ts | 5 ++--- .../src/OpenId4VcVerifierModuleConfig.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierModule.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierModule.ts index 5485787c60..22d2e313ba 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierModule.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierModule.ts @@ -43,13 +43,12 @@ export class OpenId4VcVerifierModule implements Module { } public async initialize(agentContext: AgentContext): Promise { - if (!this.config.endPointConfig) return + const endPointConfig = this.config.endpointConfig + if (!endPointConfig) return // create application/x-www-form-urlencoded parser const urlencodedParser = bodyParser.urlencoded({ extended: false }) - const endPointConfig = this.config.endPointConfig - endPointConfig.app.post(endPointConfig.verificationEndpointPath, urlencodedParser, async (req, res, next) => { try { const isVpRequest = req.body.presentation_submission !== undefined diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierModuleConfig.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierModuleConfig.ts index 1f2aac2685..d9359d7b3b 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierModuleConfig.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierModuleConfig.ts @@ -5,14 +5,14 @@ import type { Express } from 'express' export type ProofResponseHandlerReturn = { status: number } export type ProofResponseHandler = (verifiedProofResponse: VerifiedProofResponse) => Promise -export interface EndPointConfig { +export interface EndpointConfig { app: Express verificationEndpointPath: string proofResponseHandler?: ProofResponseHandler } export interface OpenId4VcVerifierModuleConfigOptions { - SessionManager?: IInMemoryVerifierSessionManager - endPointConfig?: EndPointConfig + sessionManager?: IInMemoryVerifierSessionManager + endpointConfig?: EndpointConfig } export class OpenId4VcVerifierModuleConfig { @@ -22,11 +22,11 @@ export class OpenId4VcVerifierModuleConfig { this.options = options } - public get endPointConfig() { - return this.options.endPointConfig + public get endpointConfig() { + return this.options.endpointConfig } public get sessionManager() { - return this.options.SessionManager + return this.options.sessionManager } } From d517217a70b5de7e281f6ce2e022560950da81bd Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Thu, 23 Nov 2023 14:27:33 +0100 Subject: [PATCH 073/115] feat: add openid4vci endpoints --- .../openid4vc-holder/tests/fixtures_vp.ts | 4 +- .../tests/openid4vci-holder.e2e.test.ts | 494 ++++++++++-------- .../tests/openid4vp-holder.e2e.test.ts | 29 +- packages/openid4vc-issuer/package.json | 5 +- .../src/OpenId4VcIssuerApi.ts | 21 +- .../src/OpenId4VcIssuerModuleConfig.ts | 43 +- .../src/OpenId4VcIssuerService.ts | 101 ++-- .../src/OpenId4VcIssuerServiceOptions.ts | 63 ++- .../src/issuance/OpenId4VciHolderService.ts | 15 +- .../src/issuance/utils/Formats.ts | 3 +- .../router/OpenId4VcIEndpointConfiguration.ts | 119 +++++ .../src/router/accessTokenEndpoint.ts | 107 ++++ packages/openid4vc-issuer/src/router/utils.ts | 34 ++ .../tests/openid4vc-issuer.e2e.test.ts | 14 +- 14 files changed, 750 insertions(+), 302 deletions(-) create mode 100644 packages/openid4vc-issuer/src/router/OpenId4VcIEndpointConfiguration.ts create mode 100644 packages/openid4vc-issuer/src/router/accessTokenEndpoint.ts create mode 100644 packages/openid4vc-issuer/src/router/utils.ts diff --git a/packages/openid4vc-holder/tests/fixtures_vp.ts b/packages/openid4vc-holder/tests/fixtures_vp.ts index 81b5b526da..48c63f4d32 100644 --- a/packages/openid4vc-holder/tests/fixtures_vp.ts +++ b/packages/openid4vc-holder/tests/fixtures_vp.ts @@ -1,5 +1,5 @@ export const waltPortalOpenBadgeJwt = - 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDprZXk6ejZNa29hYkE3TG10amVlQUFHS3FxY3BtaHNkYTZCczJaYXlWUzZMUmF5MmdiWFJKIn0.eyJpc3MiOiJkaWQ6a2V5Ono2TWtvYWJBN0xtdGplZUFBR0txcWNwbWhzZGE2QnMyWmF5VlM2TFJheTJnYlhSSiIsInN1YiI6ImRpZDprZXk6ejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjI3o2TWtwR1I0Z3M0UmMzWnBoNHZqOHdSbmpuQXhnQVBTeGNSOE1BVkt1dFdzcFF6YyIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQuanNvbiJdLCJpZCI6InVybjp1dWlkOjg2ODhkMWQxLTFkN2ItNDYzMC1iNTcwLTcxNGFkNTFjODk0NyIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJPcGVuQmFkZ2VDcmVkZW50aWFsIl0sIm5hbWUiOiJKRkYgeCB2Yy1lZHUgUGx1Z0Zlc3QgMyBJbnRlcm9wZXJhYmlsaXR5IiwiaXNzdWVyIjp7InR5cGUiOlsiUHJvZmlsZSJdLCJpZCI6ImRpZDprZXk6ejZNa29hYkE3TG10amVlQUFHS3FxY3BtaHNkYTZCczJaYXlWUzZMUmF5MmdiWFJKIiwibmFtZSI6IkpvYnMgZm9yIHRoZSBGdXR1cmUgKEpGRikiLCJ1cmwiOiJodHRwczovL3d3dy5qZmYub3JnLyIsImltYWdlIjoiaHR0cHM6Ly93M2MtY2NnLmdpdGh1Yi5pby92Yy1lZC9wbHVnZmVzdC0xLTIwMjIvaW1hZ2VzL0pGRl9Mb2dvTG9ja3VwLnBuZyJ9LCJpc3N1YW5jZURhdGUiOiIyMDIzLTExLTA4VDE1OjE1OjMwLjY3NDI5MzY0MFoiLCJleHBpcmF0aW9uRGF0ZSI6IjIwMjQtMTEtMDdUMTU6MTU6MzAuNjc0MzMyODgwWiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rcEdSNGdzNFJjM1pwaDR2ajh3Um5qbkF4Z0FQU3hjUjhNQVZLdXRXc3BRemMjejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjIiwidHlwZSI6WyJBY2hpZXZlbWVudFN1YmplY3QiXSwiYWNoaWV2ZW1lbnQiOnsiaWQiOiJ1cm46dXVpZDphYzI1NGJkNS04ZmFkLTRiYjEtOWQyOS1lZmQ5Mzg1MzY5MjYiLCJ0eXBlIjpbIkFjaGlldmVtZW50Il0sIm5hbWUiOiJKRkYgeCB2Yy1lZHUgUGx1Z0Zlc3QgMyBJbnRlcm9wZXJhYmlsaXR5IiwiZGVzY3JpcHRpb24iOiJUaGlzIHdhbGxldCBzdXBwb3J0cyB0aGUgdXNlIG9mIFczQyBWZXJpZmlhYmxlIENyZWRlbnRpYWxzIGFuZCBoYXMgZGVtb25zdHJhdGVkIGludGVyb3BlcmFiaWxpdHkgZHVyaW5nIHRoZSBwcmVzZW50YXRpb24gcmVxdWVzdCB3b3JrZmxvdyBkdXJpbmcgSkZGIHggVkMtRURVIFBsdWdGZXN0IDMuIiwiY3JpdGVyaWEiOnsidHlwZSI6IkNyaXRlcmlhIiwibmFycmF0aXZlIjoiV2FsbGV0IHNvbHV0aW9ucyBwcm92aWRlcnMgZWFybmVkIHRoaXMgYmFkZ2UgYnkgZGVtb25zdHJhdGluZyBpbnRlcm9wZXJhYmlsaXR5IGR1cmluZyB0aGUgcHJlc2VudGF0aW9uIHJlcXVlc3Qgd29ya2Zsb3cuIFRoaXMgaW5jbHVkZXMgc3VjY2Vzc2Z1bGx5IHJlY2VpdmluZyBhIHByZXNlbnRhdGlvbiByZXF1ZXN0LCBhbGxvd2luZyB0aGUgaG9sZGVyIHRvIHNlbGVjdCBhdCBsZWFzdCB0d28gdHlwZXMgb2YgdmVyaWZpYWJsZSBjcmVkZW50aWFscyB0byBjcmVhdGUgYSB2ZXJpZmlhYmxlIHByZXNlbnRhdGlvbiwgcmV0dXJuaW5nIHRoZSBwcmVzZW50YXRpb24gdG8gdGhlIHJlcXVlc3RvciwgYW5kIHBhc3NpbmcgdmVyaWZpY2F0aW9uIG9mIHRoZSBwcmVzZW50YXRpb24gYW5kIHRoZSBpbmNsdWRlZCBjcmVkZW50aWFscy4ifSwiaW1hZ2UiOnsiaWQiOiJodHRwczovL3czYy1jY2cuZ2l0aHViLmlvL3ZjLWVkL3BsdWdmZXN0LTMtMjAyMy9pbWFnZXMvSkZGLVZDLUVEVS1QTFVHRkVTVDMtYmFkZ2UtaW1hZ2UucG5nIiwidHlwZSI6IkltYWdlIn19fX0sImp0aSI6InVybjp1dWlkOjg2ODhkMWQxLTFkN2ItNDYzMC1iNTcwLTcxNGFkNTFjODk0NyIsImV4cCI6MTczMDk5MjUzMCwiaWF0IjoxNjk5NDU2NTMwLCJuYmYiOjE2OTk0NTY0NDB9.Qv140_WAK8jzF9ZFRi1zQPRaLpdcTmf3QSVxXjNVrtJLqwW7ePqQoplGpxuvzKGUFhjIMVIRt4EVEcU_j1mfBw' + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa3RpUVFFcW0yeWFwWEJEdDFXRVZCM2RxZ3Z5emk5NkZ1RkFOWW1yZ1RyS1Y5I3o2TWt0aVFRRXFtMnlhcFhCRHQxV0VWQjNkcWd2eXppOTZGdUZBTlltcmdUcktWOSJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiT3BlbkJhZGdlQ3JlZGVudGlhbCJdLCJjcmVkZW50aWFsU3ViamVjdCI6e319LCJpc3MiOiJkaWQ6a2V5Ono2TWt0aVFRRXFtMnlhcFhCRHQxV0VWQjNkcWd2eXppOTZGdUZBTlltcmdUcktWOSIsInN1YiI6ImRpZDprZXk6ejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjIiwibmJmIjoxNzAwNzQzMzM1fQ.OcKPyaWeVV-78BWr8N4h2Cyvjtc9jzknAqvTA77hTbKCNCEbhGboo-S6yXHLC-3NWYQ1vVcqZmdPlIOrHZ7MDw' export const waltUniversityDegreeJwt = - 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDprZXk6ejZNa29hYkE3TG10amVlQUFHS3FxY3BtaHNkYTZCczJaYXlWUzZMUmF5MmdiWFJKIn0.eyJpc3MiOiJkaWQ6a2V5Ono2TWtvYWJBN0xtdGplZUFBR0txcWNwbWhzZGE2QnMyWmF5VlM2TFJheTJnYlhSSiIsInN1YiI6ImRpZDprZXk6ejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjI3o2TWtwR1I0Z3M0UmMzWnBoNHZqOHdSbmpuQXhnQVBTeGNSOE1BVkt1dFdzcFF6YyIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvZXhhbXBsZXMvdjEiXSwiaWQiOiJ1cm46dXVpZDpmNTkyMzFhMS1jZWJkLTQyNDMtYjQwNy01OWFlOWYxYjRkMzciLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVW5pdmVyc2l0eURlZ3JlZSJdLCJpc3N1ZXIiOnsiaWQiOiJkaWQ6a2V5Ono2TWtvYWJBN0xtdGplZUFBR0txcWNwbWhzZGE2QnMyWmF5VlM2TFJheTJnYlhSSiJ9LCJpc3N1YW5jZURhdGUiOiIyMDIzLTExLTEwVDE0OjUxOjUxLjQ4NTYzNjY5M1oiLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6ejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjI3o2TWtwR1I0Z3M0UmMzWnBoNHZqOHdSbmpuQXhnQVBTeGNSOE1BVkt1dFdzcFF6YyIsImRlZ3JlZSI6eyJ0eXBlIjoiQmFjaGVsb3JEZWdyZWUiLCJuYW1lIjoiQmFjaGVsb3Igb2YgU2NpZW5jZSBhbmQgQXJ0cyJ9fX0sImp0aSI6InVybjp1dWlkOmY1OTIzMWExLWNlYmQtNDI0My1iNDA3LTU5YWU5ZjFiNGQzNyIsImlhdCI6MTY5OTYyNzkxMSwibmJmIjoxNjk5NjI3ODIxfQ.IvEhwCLBZ-zEyY1f1AV6T9tBG27f2PoFQi5rzvSNN1Io8x6f4PmtOmyNZsNLAD56pZFgyGKUJomQbQSP5thyBQ' + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa3RpUVFFcW0yeWFwWEJEdDFXRVZCM2RxZ3Z5emk5NkZ1RkFOWW1yZ1RyS1Y5I3o2TWt0aVFRRXFtMnlhcFhCRHQxV0VWQjNkcWd2eXppOTZGdUZBTlltcmdUcktWOSJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVW5pdmVyc2l0eURlZ3JlZUNyZWRlbnRpYWwiXSwiY3JlZGVudGlhbFN1YmplY3QiOnt9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdGlRUUVxbTJ5YXBYQkR0MVdFVkIzZHFndnl6aTk2RnVGQU5ZbXJnVHJLVjkiLCJzdWIiOiJkaWQ6a2V5Ono2TWtwR1I0Z3M0UmMzWnBoNHZqOHdSbmpuQXhnQVBTeGNSOE1BVkt1dFdzcFF6YyIsIm5iZiI6MTcwMDc0MzM5NH0.EhMnE349oOvzbu0rFl-m_7FOoRsB5VucLV5tUUIW0jPxkJ7J0qVLOJTXVX4KNv_N9oeP8pgTUvydd6nxB_0KCQ' diff --git a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts index 7f9329d622..2ebfe8a1ba 100644 --- a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts @@ -1,4 +1,10 @@ -import type { KeyDidCreateOptions } from '@aries-framework/core' +import type { KeyDidCreateOptions, VerificationMethod } from '@aries-framework/core' +import type { + CredentialSupported, + IssuerMetadata, + PreAuthorizedCodeFlowConfig, +} from '@aries-framework/openid4vc-issuer' +import type { Server } from 'http' import { AskarModule } from '@aries-framework/askar' import { @@ -9,9 +15,15 @@ import { W3cCredentialRecord, DidKey, ClaimFormat, + W3cCredential, + W3cIssuer, + W3cCredentialSubject, + w3cDate, } from '@aries-framework/core' import { agentDependencies } from '@aries-framework/node' +import { OpenId4VcIssuerModule } from '@aries-framework/openid4vc-issuer' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' +import express, { Router, type Express } from 'express' import nock, { cleanAll, enableNetConnect } from 'nock' import { OpenIdCredentialFormatProfile } from '../' @@ -19,42 +31,155 @@ import { OpenId4VcHolderModule } from '../src/OpenId4VcHolderModule' import { mattrLaunchpadJsonLd_draft_08, - // FIXME: we need a custom document loader for this, which is only present in AFJ core - // mattrLaunchpadJsonLd_draft_08, waltIdJffJwt_draft_08, waltIdJffJwt_draft_11, waltIssuerPortalV11, } from './fixtures' -const modules = { +const issuerPort = 1234 +const credentialIssuer = `http://localhost:${issuerPort}` + +const openBadgeCredential: CredentialSupported & { id: string } = { + id: `${credentialIssuer}/credentials/OpenBadgeCredential`, + format: 'jwt_vc_json', + types: ['VerifiableCredential', 'OpenBadgeCredential'], +} + +const universityDegreeCredential: CredentialSupported & { id: string } = { + id: `${credentialIssuer}/credentials/UniversityDegreeCredential`, + format: 'jwt_vc_json', + types: ['VerifiableCredential', 'UniversityDegreeCredential'], +} + +const universityDegreeCredentialLd: CredentialSupported & { id: string } = { + id: `${credentialIssuer}/credentials/UniversityDegreeCredentialLd`, + format: 'jwt_vc_json-ld', + types: ['VerifiableCredential', 'UniversityDegreeCredential'], +} + +const baseCredentialRequestOptions = { + scheme: 'openid-credential-offer', + baseUri: credentialIssuer, +} + +const issuerMetadata: IssuerMetadata = { + credentialIssuer, + credentialEndpoint: `${credentialIssuer}/credentials`, + tokenEndpoint: `${credentialIssuer}/token`, + credentialsSupported: [openBadgeCredential, universityDegreeCredentialLd], +} + +const holderModules = { openId4VcHolder: new OpenId4VcHolderModule(), - askar: new AskarModule({ - ariesAskar, - }), + askar: new AskarModule({ ariesAskar }), +} + +const issuerModules = { + openId4VcIssuer: new OpenId4VcIssuerModule({ issuerMetadata }), + askar: new AskarModule({ ariesAskar }), } describe('OpenId4VcHolder', () => { - let agent: Agent + let issuerApp: Express + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let issuerServer: Server + + let issuer: Agent + let issuerKid: string + let issuerDid: string + let issuerVerificationMethod: VerificationMethod + + let holder: Agent + let holderKid: string + let holderDid: string + let holderVerificationMethod: VerificationMethod + + let holderP256Kid: string + let holderP256Did: string + let holderP256VerificationMethod: VerificationMethod beforeEach(async () => { - agent = new Agent({ + issuerApp = express() + + issuer = new Agent({ config: { - label: 'OpenId4VcHolder Test23', - walletConfig: { - id: 'openid4vc-holder-test24', - key: 'openid4vc-holder-test25', - }, + label: 'OpenId4VcIssuer Test27', + walletConfig: { id: 'openid4vc-issuer-test27', key: 'openid4vc-issuer-test27' }, + }, + dependencies: agentDependencies, + modules: issuerModules, + }) + + holder = new Agent({ + config: { + label: 'OpenId4VcHolder Test27', + walletConfig: { id: 'openid4vc-holder-test27', key: 'openid4vc-holder-test27' }, }, dependencies: agentDependencies, - modules, + modules: holderModules, + }) + + await issuer.initialize() + await holder.initialize() + + const holderDidCreateResult = await holder.dids.create({ + method: 'key', + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, }) - await agent.initialize() + holderDid = holderDidCreateResult.didState.did as string + const holderDidKey = DidKey.fromDid(holderDid) + holderKid = `${holderDid}#${holderDidKey.key.fingerprint}` + + const _holderVerificationMethod = holderDidCreateResult.didState.didDocument?.dereferenceKey(holderKid, [ + 'authentication', + ]) + if (!_holderVerificationMethod) throw new Error('No verification method found') + holderVerificationMethod = _holderVerificationMethod + + const holderP256DidCreateResult = await holder.dids.create({ + method: 'key', + options: { keyType: KeyType.P256 }, + secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, + }) + + holderP256Did = holderP256DidCreateResult.didState.did as string + const holderP256DidKey = DidKey.fromDid(holderP256Did) + holderP256Kid = `${holderP256Did}#${holderP256DidKey.key.fingerprint}` + + const _holderP256VerificationMethod = holderP256DidCreateResult.didState.didDocument?.dereferenceKey( + holderP256Kid, + ['authentication'] + ) + if (!_holderP256VerificationMethod) throw new Error('No verification method found') + holderP256VerificationMethod = _holderP256VerificationMethod + + const issuerDidCreateResult = await issuer.dids.create({ + method: 'key', + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598f') }, + }) + + issuerDid = issuerDidCreateResult.didState.did as string + + const issuerDidKey = DidKey.fromDid(issuerDid) + issuerKid = `${issuerDid}#${issuerDidKey.key.fingerprint}` + const _issuerVerificationMethod = issuerDidCreateResult.didState.didDocument?.dereferenceKey(issuerKid, [ + 'authentication', + ]) + if (!_issuerVerificationMethod) throw new Error('No verification method found') + issuerVerificationMethod = _issuerVerificationMethod }) afterEach(async () => { - await agent.shutdown() - await agent.wallet.delete() + issuerServer?.close() + + await issuer.shutdown() + await issuer.wallet.delete() + + await holder.shutdown() + await holder.wallet.delete() }) describe('[DRAFT 08]: Pre-authorized flow', () => { @@ -98,22 +223,11 @@ describe('OpenId4VcHolder', () => { .post('/oidc/v1/auth/credential') .reply(200, fixture.jsonLdCredentialResponse) - const did = await agent.dids.create({ - method: 'key', - options: { keyType: KeyType.Ed25519 }, - secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, - }) - - const didKey = DidKey.fromDid(did.didState.did as string) - const kid = `${did.didState.did as string}#${didKey.key.fingerprint}` - const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) - if (!verificationMethod) throw new Error('No verification method found') - - const resolved = await agent.modules.openId4VcHolder.resolveCredentialOffer( + const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer( fixture.permanentResidentCardCredentialOffer ) - const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( resolved, { verifyCredentialStatus: false, @@ -123,7 +237,7 @@ describe('OpenId4VcHolder', () => { credentialsToRequest: resolved.credentialsToRequest.filter( (c) => c.format === OpenIdCredentialFormatProfile.LdpVc ), - proofOfPossessionVerificationMethodResolver: () => verificationMethod, + proofOfPossessionVerificationMethodResolver: () => holderVerificationMethod, } ) @@ -133,7 +247,7 @@ describe('OpenId4VcHolder', () => { expect(w3cCredentialRecord.credential.type).toEqual(['VerifiableCredential', 'PermanentResidentCard']) - expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) + expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(holderDid) }) it('[DRAFT 08]: Should successfully execute the pre-authorized flow using a did:key P256 subject and JWT credential', async () => { @@ -156,30 +270,15 @@ describe('OpenId4VcHolder', () => { .post('/credential') .reply(200, fixture.credentialResponse) - const did = await agent.dids.create({ - method: 'key', - options: { - keyType: KeyType.P256, - }, - secret: { - privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), - }, - }) - - const didKey = DidKey.fromDid(did.didState.did as string) - const kid = `${didKey.did}#${didKey.key.fingerprint}` - const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) - if (!verificationMethod) throw new Error('No verification method found') - - const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer( + const resolvedCredentialOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer( fixture.credentialOffer ) - const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( resolvedCredentialOffer, { allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], - proofOfPossessionVerificationMethodResolver: () => verificationMethod, + proofOfPossessionVerificationMethodResolver: () => holderP256VerificationMethod, verifyCredentialStatus: false, credentialsToRequest: resolvedCredentialOffer.credentialsToRequest.filter((credential) => { return credential.format === OpenIdCredentialFormatProfile.JwtVcJson @@ -196,7 +295,7 @@ describe('OpenId4VcHolder', () => { 'VerifiableId', ]) - expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) + expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(holderP256Did) }) }) @@ -247,130 +346,33 @@ describe('OpenId4VcHolder', () => { .post('/oidc/v1/auth/credential') .reply(200, fixture.jsonLdCredentialResponse) - const did = await agent.dids.create({ - method: 'key', - options: { keyType: KeyType.Ed25519 }, - secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, - }) - - const didKey = DidKey.fromDid(did.didState.did as string) - const kid = `${did.didState.did as string}#${didKey.key.fingerprint}` - const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) - if (!verificationMethod) throw new Error('No verification method found') - const clientId = 'test-client' const redirectUri = 'https://example.com/cb' - const resolvedOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer( + const resolvedOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer( fixture.credentialOfferAuthorizationCodeFlow ) - const resolvedAuthRequest = await agent.modules.openId4VcHolder.resolveAuthorizationRequest(resolvedOffer, { + const resolvedAuthRequest = await holder.modules.openId4VcHolder.resolveAuthorizationRequest(resolvedOffer, { clientId, redirectUri, scope: ['openid'], }) await expect( - agent.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( + holder.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( resolvedOffer, resolvedAuthRequest, 'code', { verifyCredentialStatus: false, - proofOfPossessionVerificationMethodResolver: () => verificationMethod, + proofOfPossessionVerificationMethodResolver: () => holderVerificationMethod, allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], } ) ).rejects.toThrow() }) - // Need custom document loader for this - xit('[DRAFT 08]: should successfully execute request a credential', async () => { - const fixture = mattrLaunchpadJsonLd_draft_08 - - // setup temporary redirect mock - nock('https://launchpad.mattrlabs.com') - .get('/.well-known/openid-credential-issuer') - .reply(307, undefined, { - Location: 'https://launchpad.vii.electron.mattrlabs.io/.well-known/openid-credential-issuer', - }) - .get('/.well-known/openid-configuration') - .reply(404) - .get('/.well-known/openid-configuration') - .reply(404) - .get('/.well-known/openid-credential-issuer') - .reply(200, fixture.getMetadataResponse) - .get('/.well-known/oauth-authorization-server') - .reply(404) - .get('/.well-known/oauth-authorization-server') - .reply(404) - - // setup server metadata response - nock('https://launchpad.vii.electron.mattrlabs.io') - .get('/.well-known/did.json') - .reply(200, fixture.wellKnownDid) - .get('/.well-known/did.json') - .reply(200, fixture.wellKnownDid) - .get('/.well-known/openid-credential-issuer') - .reply(200, fixture.getMetadataResponse) - .get('/.well-known/openid-configuration') - .reply(404) - .get('/.well-known/oauth-authorization-server') - .reply(404) - - // setup access token response - .post('/oidc/v1/auth/token') - .reply(200, fixture.acquireAccessTokenResponse) - - // setup credential request response - .post('/oidc/v1/auth/credential') - .reply(200, fixture.jsonLdCredentialResponse) - - const did = await agent.dids.create({ - method: 'key', - options: { keyType: KeyType.Ed25519 }, - secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, - }) - - const didKey = DidKey.fromDid(did.didState.did as string) - const kid = `${did.didState.did as string}#${didKey.key.fingerprint}` - const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) - if (!verificationMethod) throw new Error('No verification method found') - - const opts = { - clientId: 'test-client', - redirectUri: 'https://example.com/cb', - scope: ['TestCredential'], - } - - const resolvedOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer( - fixture.credentialOfferAuthorizationCodeFlow - ) - - const resolvedAuthRequest = await agent.modules.openId4VcHolder.resolveAuthorizationRequest(resolvedOffer, opts) - - const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( - resolvedOffer, - resolvedAuthRequest, - 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJiMGUxNjc4NS1kNzIyLTQyYTUtYTA0Zi00YmVhYjI4ZTAzZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.ibEpHFaHFBLWyhEf4SotDQZBeh_FMrfncWapNox1Iv1kdQWQ2cLQeS1VrCyVmPsbx0tN2MAyDFG7DnAaq8MiAA', - { - verifyCredentialStatus: false, - proofOfPossessionVerificationMethodResolver: () => verificationMethod, - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - } - ) - - expect(w3cCredentialRecords).toHaveLength(1) - const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord - expect(w3cCredentialRecord).toBeInstanceOf(W3cCredentialRecord) - - expect(w3cCredentialRecord.credential.type).toEqual(['VerifiableCredential', 'PermanentResidentCard']) - - expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) - }) - }) - describe('[DRAFT 11]: Pre-authorized flow', () => { afterEach(() => { cleanAll() @@ -393,26 +395,15 @@ describe('OpenId4VcHolder', () => { .get('/.well-known/oauth-authorization-server') .reply(404) - const did = await agent.dids.create({ - method: 'key', - options: { keyType: KeyType.P256 }, - secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, - }) - - const didKey = DidKey.fromDid(did.didState.did as string) - const kid = `${didKey.did}#${didKey.key.fingerprint}` - const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) - if (!verificationMethod) throw new Error('No verification method found') - - const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer( + const resolvedCredentialOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer( fixture.credentialOffer ) - const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( resolvedCredentialOffer, { allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], - proofOfPossessionVerificationMethodResolver: () => verificationMethod, + proofOfPossessionVerificationMethodResolver: () => holderVerificationMethod, verifyCredentialStatus: false, credentialsToRequest: [], } @@ -436,18 +427,7 @@ describe('OpenId4VcHolder', () => { // setup credential request response httpMock.post('/credential').reply(200, fixture.credentialResponse) - const did = await agent.dids.create({ - method: 'key', - options: { keyType: KeyType.P256 }, - secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, - }) - - const didKey = DidKey.fromDid(did.didState.did as string) - const kid = `${didKey.did}#${didKey.key.fingerprint}` - const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) - if (!verificationMethod) throw new Error('No verification method found') - - const resolved = await agent.modules.openId4VcHolder.resolveCredentialOffer(fixture.credentialOffer) + const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer(fixture.credentialOffer) expect(resolved.credentialsToRequest).toHaveLength(2) const selectedCredentialsForRequest = resolved.credentialsToRequest.filter((credential) => { @@ -458,11 +438,11 @@ describe('OpenId4VcHolder', () => { expect(selectedCredentialsForRequest).toHaveLength(1) - const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( resolved, { allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], - proofOfPossessionVerificationMethodResolver: () => verificationMethod, + proofOfPossessionVerificationMethodResolver: () => holderP256VerificationMethod, verifyCredentialStatus: false, credentialsToRequest: selectedCredentialsForRequest, } @@ -478,7 +458,7 @@ describe('OpenId4VcHolder', () => { 'VerifiableId', ]) - expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) + expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(holderP256Did) }) xit('[DRAFT 11]: Should successfully execute the pre-authorized flow using a single offered credential a did:key EdDSA subject and JsonLd format', async () => { @@ -496,17 +476,7 @@ describe('OpenId4VcHolder', () => { // setup credential request response httpMock.post('/credential').reply(200, fixture.jsonLdCredentialResponse) - const did = await agent.dids.create({ - method: 'key', - options: { keyType: KeyType.Ed25519 }, - secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, - }) - - const didKey = DidKey.fromDid(did.didState.did as string) - const kid = `${didKey.did}#${didKey.key.fingerprint}` - const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) - if (!verificationMethod) throw new Error('No verification method found') - const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer( + const resolvedCredentialOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer( fixture.credentialOffer ) @@ -519,11 +489,11 @@ describe('OpenId4VcHolder', () => { expect(selectedCredentialsForRequest).toHaveLength(1) - const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( resolvedCredentialOffer, { allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - proofOfPossessionVerificationMethodResolver: () => verificationMethod, + proofOfPossessionVerificationMethodResolver: () => holderVerificationMethod, verifyCredentialStatus: false, credentialsToRequest: selectedCredentialsForRequest, } @@ -535,7 +505,7 @@ describe('OpenId4VcHolder', () => { expect(w3cCredentialRecord.credential.type).toEqual(['VerifiableCredential', 'PermanentResidentCard']) - expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(did.didState.did) + expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(holderDid) }) xit('[DRAFT 11]: Should successfully execute the pre-authorized for multiple credentials of different formats using a did:key EdDsa subject', async () => { @@ -554,27 +524,17 @@ describe('OpenId4VcHolder', () => { httpMock.post('/credential').reply(200, fixture.credentialResponse) httpMock.post('/credential').reply(200, fixture.jsonLdCredentialResponse) - const did = await agent.dids.create({ - method: 'key', - options: { keyType: KeyType.Ed25519 }, - secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, - }) - - const didKey = DidKey.fromDid(did.didState.did as string) - const kid = `${didKey.did}#${didKey.key.fingerprint}` - const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) - if (!verificationMethod) throw new Error('No verification method found') - const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer( + const resolvedCredentialOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer( fixture.credentialOffer ) expect(resolvedCredentialOffer.credentialsToRequest).toHaveLength(2) - const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( resolvedCredentialOffer, { allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - proofOfPossessionVerificationMethodResolver: () => verificationMethod, + proofOfPossessionVerificationMethodResolver: () => holderVerificationMethod, verifyCredentialStatus: false, } ) @@ -593,7 +553,7 @@ describe('OpenId4VcHolder', () => { const w3cCredentialRecord1 = w3cCredentialRecords[1] as W3cCredentialRecord expect(w3cCredentialRecord1.credential.claimFormat).toEqual(ClaimFormat.LdpVc) expect(w3cCredentialRecord1.credential.type).toEqual(['VerifiableCredential', 'PermanentResidentCard']) - expect(w3cCredentialRecord1.credential.credentialSubjectIds[0]).toEqual(did.didState.did) + expect(w3cCredentialRecord1.credential.credentialSubjectIds[0]).toEqual(holderDid) }) it('authorization code flow https://portal.walt.id/', async () => { @@ -618,22 +578,10 @@ describe('OpenId4VcHolder', () => { .get('/.well-known/oauth-authorization-server') .reply(404) - const did = await agent.dids.create({ - method: 'key', - options: { keyType: KeyType.Ed25519 }, - secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, - }) - const credentialOffer = `openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.portal.walt.id%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22OpenBadgeCredential%22%5D%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fpurl.imsglobal.org%2Fspec%2Fob%2Fv3p0%2Fcontext.json%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22OpenBadgeCredential%22%5D%7D%7D%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22b0e16785-d722-42a5-a04f-4beab28e03ea%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJiMGUxNjc4NS1kNzIyLTQyYTUtYTA0Zi00YmVhYjI4ZTAzZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.ibEpHFaHFBLWyhEf4SotDQZBeh_FMrfncWapNox1Iv1kdQWQ2cLQeS1VrCyVmPsbx0tN2MAyDFG7DnAaq8MiAA%22%2C%22user_pin_required%22%3Afalse%7D%7D%7D` + const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) - const didKey = DidKey.fromDid(did.didState.did as string) - const kid = `${didKey.did}#${didKey.key.fingerprint}` - const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) - if (!verificationMethod) throw new Error('No verification method found') - - const resolved = await agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) - - const resolvedAuthorizationRequest = await agent.modules.openId4VcHolder.resolveAuthorizationRequest(resolved, { + const resolvedAuthorizationRequest = await holder.modules.openId4VcHolder.resolveAuthorizationRequest(resolved, { clientId: 'test-client', redirectUri: 'http://blank', scope: ['openid', 'OpenBadgeCredential'], @@ -642,28 +590,110 @@ describe('OpenId4VcHolder', () => { const code = 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJiMGUxNjc4NS1kNzIyLTQyYTUtYTA0Zi00YmVhYjI4ZTAzZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.ibEpHFaHFBLWyhEf4SotDQZBeh_FMrfncWapNox1Iv1kdQWQ2cLQeS1VrCyVmPsbx0tN2MAyDFG7DnAaq8MiAA' - const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( + const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( resolved, resolvedAuthorizationRequest, code, { allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - proofOfPossessionVerificationMethodResolver: () => verificationMethod, + proofOfPossessionVerificationMethodResolver: () => holderVerificationMethod, verifyCredentialStatus: false, } ) expect(w3cCredentialRecords).toHaveLength(1) }) + + it('e2e flow with issuer endpoints requesting multiple credentials', async () => { + const router = Router() + await issuer.modules.openId4VcIssuer.configureRouter(router, { + metadataEndpointConfig: { enabled: true }, + accessTokenEndpointConfig: { + enabled: true, + preAuthorizedCodeExpirationDuration: 50, + verificationMethod: issuerVerificationMethod, + }, + credentialEndpointConfig: { + enabled: true, + verificationMethod: issuerVerificationMethod, + credentialRequestToCredentialMapper: async (credentialRequest) => { + if (credentialRequest.format === 'jwt_vc_json' && credentialRequest.types.includes('OpenBadgeCredential')) { + return new W3cCredential({ + type: openBadgeCredential.types, + issuer: new W3cIssuer({ id: issuerDid }), + credentialSubject: new W3cCredentialSubject({ id: holderDid }), + issuanceDate: w3cDate(Date.now()), + }) + } + + if ( + credentialRequest.format === 'jwt_vc_json' && + credentialRequest.types.includes('UniversityDegreeCredential') + ) { + return new W3cCredential({ + type: universityDegreeCredential.types, + issuer: new W3cIssuer({ id: issuerDid }), + credentialSubject: new W3cCredentialSubject({ id: holderDid }), + issuanceDate: w3cDate(Date.now()), + }) + } + throw new Error('Invalid request') + }, + }, + }) + + issuerApp.use('/', router) + issuerServer = issuerApp.listen(issuerPort) + + const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { + preAuthorizedCode: '123456789', + userPinRequired: false, + } + + const { credentialOfferRequest } = await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest( + [ + openBadgeCredential.id, + { + format: universityDegreeCredential.format, + types: universityDegreeCredential.types, + }, + ], + { + preAuthorizedCodeFlowConfig, + ...baseCredentialRequestOptions, + baseUri: '', + } + ) + + const resolvedCredentialOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer( + credentialOfferRequest + ) + + const credentials = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + resolvedCredentialOffer, + { + proofOfPossessionVerificationMethodResolver: async () => { + return holderVerificationMethod + }, + } + ) + + expect(credentials).toHaveLength(2) + expect(credentials[0]).toBeInstanceOf(W3cCredentialRecord) + expect(credentials[0].credential.type).toHaveLength(2) + expect(credentials[1].credential.type).toHaveLength(2) + + if (credentials[0].credential.type.includes('OpenBadgeCredential')) { + expect(credentials[0].credential.type).toEqual(['VerifiableCredential', 'OpenBadgeCredential']) + expect(credentials[1].credential.type).toEqual(['VerifiableCredential', 'UniversityDegreeCredential']) + } else { + expect(credentials[1].credential.type).toEqual(['VerifiableCredential', 'OpenBadgeCredential']) + expect(credentials[0].credential.type).toEqual(['VerifiableCredential', 'UniversityDegreeCredential']) + } + }) }) //it('authorization code flow https://portal.walt.id/', async () => { - // const did = await agent.dids.create({ - // method: 'key', - // options: { keyType: KeyType.Ed25519 }, - // secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, - // }) - // const credentialOffer = `` // const didKey = DidKey.fromDid(did.didState.did as string) diff --git a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts index a2fc6dc8eb..aa39b2c9ba 100644 --- a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts @@ -18,8 +18,6 @@ import { waltPortalOpenBadgeJwt, waltUniversityDegreeJwt } from './fixtures_vp' // id id%22%3A%22test%22%2C%22 // * = %2A -// TODO: use proper credential - // TODO: error on sphereon lib PR opened // TODO: walt issued credentials verification fails due to some time issue || //throw new Error(`Inconsistent issuance dates between JWT claim (${nbfDateAsStr}) and VC value (${issuanceDate})`); // TODO: error walt no id in presentation definition @@ -116,9 +114,8 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const createVerifierModules = (verifierApp: Express) => { const modules = { - openId4VcHolder: new OpenId4VcHolderModule(), openId4VcVerifier: new OpenId4VcVerifierModule({ - endPointConfig: { + endpointConfig: { app: verifierApp, verificationEndpointPath, proofResponseHandler: mockFunction, @@ -474,8 +471,26 @@ describe('OpenId4VcHolder | OpenID4VP', () => { expect(submission?.submissionData.definition_id).toBe('Combined') expect(submission?.presentations).toHaveLength(1) expect(submission?.presentations[0].vcs).toHaveLength(2) - expect(submission?.presentations[0].vcs[0].credential.type).toContain('OpenBadgeCredential') - expect(submission?.presentations[0].vcs[1].credential.type).toContain('UniversityDegree') + + if (submission?.presentations[0].vcs[0].credential.type.includes('OpenBadgeCredential')) { + expect(submission?.presentations[0].vcs[0].credential.type).toEqual([ + 'VerifiableCredential', + 'OpenBadgeCredential', + ]) + expect(submission?.presentations[0].vcs[1].credential.type).toEqual([ + 'VerifiableCredential', + 'UniversityDegreeCredential', + ]) + } else { + expect(submission?.presentations[0].vcs[1].credential.type).toEqual([ + 'VerifiableCredential', + 'OpenBadgeCredential', + ]) + expect(submission?.presentations[0].vcs[0].credential.type).toEqual([ + 'VerifiableCredential', + 'UniversityDegreeCredential', + ]) + } await waitForMockFunction() expect(mockFunction).toBeCalledWith({ @@ -574,7 +589,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { expect(submission?.presentationDefinitions).toHaveLength(1) expect(submission?.submissionData.definition_id).toBe('OpenBadgeCredential') expect(submission?.presentations).toHaveLength(1) - expect(submission?.presentations[0].vcs[0].credential.type).toContain('OpenBadgeCredential') + expect(submission?.presentations[0].vcs[0].credential.type).toEqual(['VerifiableCredential', 'OpenBadgeCredential']) await waitForMockFunction() expect(mockFunction).toBeCalledWith({ diff --git a/packages/openid4vc-issuer/package.json b/packages/openid4vc-issuer/package.json index 648f094239..932f311ca4 100644 --- a/packages/openid4vc-issuer/package.json +++ b/packages/openid4vc-issuer/package.json @@ -29,11 +29,14 @@ "@sphereon/oid4vci-issuer": "^0.8.1", "@sphereon/oid4vci-common": "^0.8.1", "@sphereon/oid4vci-client": "^0.8.1", - "@sphereon/ssi-types": "^0.17.5" + "@sphereon/ssi-types": "^0.17.5", + "body-parser": "^1.20.2" }, "devDependencies": { "@aries-framework/node": "^0.4.2", "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", + "@types/body-parser": "^1.19.5", + "@types/express": "^4.17.21", "nock": "^13.3.0", "rimraf": "^4.4.0", "typescript": "~4.9.5" diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts index 935ce00581..a59be22cc6 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts @@ -4,7 +4,9 @@ import type { CredentialOfferAndRequest, OfferedCredential, CredentialOfferPayloadV1_0_11, + EndpointConfig, } from './OpenId4VcIssuerServiceOptions' +import type { Router } from 'express' import { injectable, AgentContext } from '@aries-framework/core' @@ -44,7 +46,7 @@ export class OpenId4VcIssuerApi { offeredCredentials: OfferedCredential[], options: CreateCredentialOfferAndRequestOptions ): Promise { - return await this.openId4VcIssuerService.createCredentialOfferAndReqeust( + return await this.openId4VcIssuerService.createCredentialOfferAndRequest( this.agentContext, offeredCredentials, options @@ -67,11 +69,22 @@ export class OpenId4VcIssuerApi { /** * This function creates a response which can be send to the holder after receiving a credential issuance request. * - * @param {string} options.credentialRequest - The credential request, for which to create a response. - * @param {string} options.credential - The credential to be issued. - * @param {IssuerMetadata} options.issuerMetadata - Metadata about the issuer. + * @param options.credentialRequest - The credential request, for which to create a response. + * @param options.credential - The credential to be issued. + * @param options.issuerMetadata - Metadata about the issuer. */ public async createIssueCredentialResponse(options: CreateIssueCredentialResponseOptions) { return await this.openId4VcIssuerService.createIssueCredentialResponse(this.agentContext, options) } + + /** + * Configures the enabled endpoints for the given router, as specified in @link https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html + * + * @param router - The router to configure. + * @param endpointConfig - The endpoint configuration. + * @returns The configured router. + */ + public async configureRouter(router: Router, endpointConfig: EndpointConfig) { + return this.openId4VcIssuerService.configureRouter(this.agentContext, router, endpointConfig) + } } diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerModuleConfig.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerModuleConfig.ts index c132b5f0a6..b6666ee7e5 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerModuleConfig.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerModuleConfig.ts @@ -1,33 +1,56 @@ import type { IssuerMetadata } from './OpenId4VcIssuerServiceOptions' import type { CNonceState, CredentialOfferSession, IStateManager, URIState } from '@sphereon/oid4vci-common' +import { MemoryStates } from '@sphereon/oid4vci-issuer' + export interface OpenId4VcIssuerModuleConfigOptions { issuerMetadata: IssuerMetadata cNonceStateManager?: IStateManager credentialOfferSessionManager?: IStateManager uriStateManager?: IStateManager + + cNonceExpiresIn?: number + tokenExpiresIn?: number } export class OpenId4VcIssuerModuleConfig { - private options: OpenId4VcIssuerModuleConfigOptions + private _issuerMetadata: IssuerMetadata + private _cNonceStateManager: IStateManager + private _credentialOfferSessionManager: IStateManager + private _uriStateManager: IStateManager + private _cNonceExpiresIn: number + private _tokenExpiresIn: number public constructor(options: OpenId4VcIssuerModuleConfigOptions) { - this.options = options + this._issuerMetadata = options.issuerMetadata + this._cNonceStateManager = options.cNonceStateManager ?? new MemoryStates() + this._credentialOfferSessionManager = options.credentialOfferSessionManager ?? new MemoryStates() + this._uriStateManager = options.uriStateManager ?? new MemoryStates() + this._cNonceExpiresIn = options.cNonceExpiresIn ?? 5 * 60 * 1000 // 5 minutes + this._tokenExpiresIn = options.tokenExpiresIn ?? 3 * 60 * 1000 // 3 minutes + } + + public get issuerMetadata(): IssuerMetadata { + return this._issuerMetadata + } + + public get cNonceStateManager(): IStateManager { + return this._cNonceStateManager } - public get issuerMetadata() { - return this.options.issuerMetadata + public get credentialOfferSessionManager(): IStateManager { + return this._credentialOfferSessionManager } - public get cNonceStateManager() { - return this.options.cNonceStateManager + public get uriStateManager(): IStateManager { + return this._uriStateManager } - public get credentialOfferSessionManager() { - return this.options.credentialOfferSessionManager + public get cNonceExpiresIn(): number { + return this._cNonceExpiresIn } - public get uriStateManager() { - return this.options.uriStateManager + public get tokenExpiresIn(): number { + return this._tokenExpiresIn } } diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts index 58d6ebb2fb..0b89de89e0 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts @@ -3,10 +3,10 @@ import type { AuthorizationCodeFlowConfig, PreAuthorizedCodeFlowConfig, OfferedCredential, - IssuerMetadata, CreateIssueCredentialResponseOptions, CredentialSupported, CredentialOfferAndRequest, + EndpointConfig, } from './OpenId4VcIssuerServiceOptions' import type { AgentContext, @@ -24,10 +24,6 @@ import type { CredentialOfferFormat, CredentialRequestV1_0_11, CredentialOfferPayloadV1_0_11, - IStateManager, - CNonceState, - CredentialOfferSession, - URIState, CredentialSupported as SphereonCredentialSupported, } from '@sphereon/oid4vci-common' import type { @@ -36,6 +32,7 @@ import type { CredentialSignerCallback, } from '@sphereon/oid4vci-issuer' import type { ICredential, W3CVerifiableCredential as SphereonW3cVerifiableCredential } from '@sphereon/ssi-types' +import type { Router } from 'express' import { AriesFrameworkError, @@ -56,9 +53,15 @@ import { equalsIgnoreOrder, } from '@aries-framework/core' import { IssueStatus } from '@sphereon/oid4vci-common' -import { MemoryStates, VcIssuerBuilder } from '@sphereon/oid4vci-issuer' +import { VcIssuerBuilder } from '@sphereon/oid4vci-issuer' +import bodyParser from 'body-parser' import { OpenId4VcIssuerModuleConfig } from './OpenId4VcIssuerModuleConfig' +import { + configureAccessTokenEndpoint, + configureCredentialEndpoint, + configureIssuerMetadataEndpoint, +} from './router/OpenId4VcIEndpointConfiguration' // TODO: duplicate function getSphereonW3cVerifiableCredential( @@ -83,23 +86,22 @@ export class OpenId4VcIssuerService { private logger: Logger private w3cCredentialService: W3cCredentialService private jwsService: JwsService - private cNonceExpiresIn: number = 5 * 60 * 1000 // 5 minutes - private tokenExpiresIn: number = 3 * 60 * 1000 // 3 minutes - private issuerMetadata: IssuerMetadata - private _cNonceStateManager: IStateManager - private _credentialOfferSessionManager: IStateManager - private _uriStateManager: IStateManager + private openId4VcIssuerModuleConfig: OpenId4VcIssuerModuleConfig + + public get issuerMetadata() { + return this.openId4VcIssuerModuleConfig.issuerMetadata + } public get cNonceStateManager() { - return this._cNonceStateManager + return this.openId4VcIssuerModuleConfig.cNonceStateManager } public get credentialOfferSessionManager() { - return this._credentialOfferSessionManager + return this.openId4VcIssuerModuleConfig.credentialOfferSessionManager } public get uriStateManager() { - return this._uriStateManager + return this.openId4VcIssuerModuleConfig.uriStateManager } public constructor( @@ -110,12 +112,8 @@ export class OpenId4VcIssuerService { ) { this.w3cCredentialService = w3cCredentialService this.logger = logger - this.issuerMetadata = openId4VcIssuerModuleConfig.issuerMetadata + this.openId4VcIssuerModuleConfig = openId4VcIssuerModuleConfig this.jwsService = jwsService - this._cNonceStateManager = openId4VcIssuerModuleConfig.cNonceStateManager ?? new MemoryStates() - this._credentialOfferSessionManager = - openId4VcIssuerModuleConfig.credentialOfferSessionManager ?? new MemoryStates() - this._uriStateManager = openId4VcIssuerModuleConfig.uriStateManager ?? new MemoryStates() } private getProofTypeForLdpVc(agentContext: AgentContext, verificationMethod: VerificationMethod) { @@ -230,10 +228,10 @@ export class OpenId4VcIssuerService { .withTokenEndpoint(tokenEndpoint) // FIXME: currently credentialsSupported is not typed correctly .withCredentialsSupported(credentialsSupported as SphereonCredentialSupported[]) - .withCNonceExpiresIn(this.cNonceExpiresIn) // 5 minutes - .withCNonceStateManager(this._cNonceStateManager) - .withCredentialOfferStateManager(this._credentialOfferSessionManager) - .withCredentialOfferURIStateManager(this._uriStateManager) + .withCNonceExpiresIn(this.openId4VcIssuerModuleConfig.cNonceExpiresIn) + .withCNonceStateManager(this.cNonceStateManager) + .withCredentialOfferStateManager(this.credentialOfferSessionManager) + .withCredentialOfferURIStateManager(this.uriStateManager) .withJWTVerifyCallback(this.getJwtVerifyCallback(agentContext)) .withCredentialSignerCallback(() => { throw new AriesFrameworkError('this should never ba called') @@ -325,7 +323,7 @@ export class OpenId4VcIssuerService { return [...credentialsReferencingCredentialsSupported, ...inlineCredentialOffers] } - public async createCredentialOfferAndReqeust( + public async createCredentialOfferAndRequest( agentContext: AgentContext, offeredCredentials: OfferedCredential[], options: CreateCredentialOfferAndRequestOptions @@ -357,7 +355,7 @@ export class OpenId4VcIssuerService { } private async getCredentialOfferSessionFromUri(uri: string) { - const uriState = await this._uriStateManager.get(uri) + const uriState = await this.uriStateManager.get(uri) if (!uriState) throw new AriesFrameworkError(`Credential offer uri '${uri}' not found.`) const credentialOfferSessionId = uriState.preAuthorizedCode ?? uriState.issuerState @@ -368,7 +366,7 @@ export class OpenId4VcIssuerService { ) } - const credentialOfferSession = await this._credentialOfferSessionManager.get(credentialOfferSessionId) + const credentialOfferSession = await this.credentialOfferSessionManager.get(credentialOfferSessionId) if (!credentialOfferSession) throw new AriesFrameworkError( `Credential offer session for '${uri}' with id '${credentialOfferSessionId}' not found.` @@ -382,7 +380,7 @@ export class OpenId4VcIssuerService { credentialOfferSession.lastUpdatedAt = +new Date() credentialOfferSession.status = IssueStatus.OFFER_URI_RETRIEVED - await this._credentialOfferSessionManager.set(credentialOfferSessionId, credentialOfferSession) + await this.credentialOfferSessionManager.set(credentialOfferSessionId, credentialOfferSession) return credentialOfferSession.credentialOffer.credential_offer } @@ -455,13 +453,17 @@ export class OpenId4VcIssuerService { ) { const { credentialRequest, credential, verificationMethod } = options + if (!credentialRequest.proof) { + throw new AriesFrameworkError('No proof defined in the credentialRequest.') + } + const issuerMetadata = options.issuerMetadata ?? this.issuerMetadata const vcIssuer = this.getVcIssuer(agentContext, issuerMetadata) const issueCredentialResponse = await vcIssuer.issueCredential({ credentialRequest, - tokenExpiresIn: this.tokenExpiresIn, - cNonceExpiresIn: this.cNonceExpiresIn, + tokenExpiresIn: this.openId4VcIssuerModuleConfig.tokenExpiresIn, + cNonceExpiresIn: this.openId4VcIssuerModuleConfig.cNonceExpiresIn, credentialDataSupplier: this.getCredentialDataSupplier( agentContext, credential, @@ -484,4 +486,43 @@ export class OpenId4VcIssuerService { return issueCredentialResponse } + + public configureRouter = (agentContext: AgentContext, router: Router, endpointConfig: EndpointConfig) => { + // parse application/x-www-form-urlencoded + router.use(bodyParser.urlencoded({ extended: false })) + + // parse application/json + router.use(bodyParser.json()) + + if (endpointConfig.metadataEndpointConfig?.enabled) { + configureIssuerMetadataEndpoint(router, this.logger, { + ...endpointConfig.metadataEndpointConfig, + issuerMetadata: this.issuerMetadata, + }) + } + + if (endpointConfig.accessTokenEndpointConfig?.enabled) { + configureAccessTokenEndpoint(agentContext, router, this.logger, { + ...endpointConfig.accessTokenEndpointConfig, + issuerMetadata: this.issuerMetadata, + cNonceStateManager: this.cNonceStateManager, + cNonceExpiresIn: this.openId4VcIssuerModuleConfig.cNonceExpiresIn, + tokenExpiresIn: this.openId4VcIssuerModuleConfig.tokenExpiresIn, + credentialOfferSessionManager: this.credentialOfferSessionManager, + }) + } + + if (endpointConfig.credentialEndpointConfig?.enabled) { + configureCredentialEndpoint(agentContext, router, this.logger, { + ...endpointConfig.credentialEndpointConfig, + issuerMetadata: this.issuerMetadata, + createIssueCredentialResponse: (agentContext, options) => { + const issuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService) + return issuerService.createIssueCredentialResponse(agentContext, options) + }, + }) + } + + return router + } } diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts index c620b670e5..0e83189516 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts @@ -65,11 +65,68 @@ export type CredentialOfferAndRequest = { credentialOfferRequest: string } -export type CredentialRequest = CredentialRequestV1_0_11 & { proof: ProofOfPossession } - export interface CreateIssueCredentialResponseOptions { - credentialRequest: CredentialRequest + credentialRequest: CredentialRequestV1_0_11 credential: W3cCredential verificationMethod: VerificationMethod issuerMetadata?: IssuerMetadata } + +export { CredentialResponse } from '@sphereon/oid4vci-common' + +export interface MetadataEndpointConfig { + /** + * Configures the router to expose the m3tadata endpoint. + */ + enabled: boolean +} + +export interface AccessTokenEndpointConfig { + /** + * Configures the router to expose the access token endpoint. + */ + enabled: boolean + + /** + * The minimum amount of time in seconds that the client SHOULD wait between polling requests to the Token Endpoint in the Pre-Authorized Code Flow. + * If no value is provided, clients MUST use 5 as the default. + */ + interval?: number + + /** + * The verification method to be used to sign access token. + */ + verificationMethod: VerificationMethod + + /** + * The maximum amount of time in seconds that the pre-authorized code is valid. + */ + preAuthorizedCodeExpirationDuration: number +} + +export type CredentialRequestToCredentialMapper = ( + credentialRequest: CredentialRequestV1_0_11 +) => Promise + +export interface CredentialEndpointConfig { + /** + * Configures the router to expose the credential endpoint. + */ + enabled: boolean + + /** + * The verification method to be used to sign the credential. + */ + verificationMethod: VerificationMethod + + /** + * A function mapping a credential request to the credential to be issued. + */ + credentialRequestToCredentialMapper: CredentialRequestToCredentialMapper +} + +export interface EndpointConfig { + metadataEndpointConfig?: MetadataEndpointConfig + accessTokenEndpointConfig?: AccessTokenEndpointConfig + credentialEndpointConfig?: CredentialEndpointConfig +} diff --git a/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderService.ts b/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderService.ts index 96b7dfc1b3..3340c11991 100644 --- a/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderService.ts +++ b/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderService.ts @@ -481,6 +481,8 @@ export class OpenId4VciHolderService { const receivedCredentials: W3cCredentialRecord[] = [] + let newCNonce: string | undefined + // Loop through all the credentialTypes in the credential offer for (const credentialWithMetadata of credentialsToRequestWithMetadata ?? offeredCredentialsWithMetadata) { // Get all options for the credential request (such as which kid to use, the signature algorithm, etc) @@ -496,7 +498,7 @@ export class OpenId4VciHolderService { } // Create the proof of possession - const proofInput = await ProofOfPossessionBuilder.fromAccessTokenResponse({ + const proofOfPossessionBuilder = ProofOfPossessionBuilder.fromAccessTokenResponse({ accessTokenResponse: accessToken, callbacks, version, @@ -505,9 +507,11 @@ export class OpenId4VciHolderService { .withAlg(signatureAlgorithm) .withClientId(verificationMethod.controller) .withKid(verificationMethod.id) - .build() - this.logger.debug('Generated JWS', proofInput) + if (newCNonce) proofOfPossessionBuilder.withAccessTokenNonce(newCNonce) + + const proofOfPossession = await proofOfPossessionBuilder.build() + this.logger.debug('Generated JWS', proofOfPossession) // Acquire the credential const credentialRequestBuilder = new CredentialRequestClientBuilder() @@ -531,11 +535,13 @@ export class OpenId4VciHolderService { const credentialRequestClient = credentialRequestBuilder.build() const credentialResponse = await credentialRequestClient.acquireCredentialsUsingProof({ - proofInput, + proofInput: proofOfPossession, credentialTypes, format: originalFormat, }) + newCNonce = credentialResponse.successBody?.c_nonce + const credential = await this.handleCredentialResponse(agentContext, credentialResponse, { verifyCredentialStatus: verifyCredentialStatus ?? false, }) @@ -713,7 +719,6 @@ export class OpenId4VciHolderService { supportsAllDidMethods, } } - private async handleCredentialResponse( agentContext: AgentContext, credentialResponse: OpenIDResponse, diff --git a/packages/openid4vc-issuer/src/issuance/utils/Formats.ts b/packages/openid4vc-issuer/src/issuance/utils/Formats.ts index 77a8259f9d..89cb688c86 100644 --- a/packages/openid4vc-issuer/src/issuance/utils/Formats.ts +++ b/packages/openid4vc-issuer/src/issuance/utils/Formats.ts @@ -1,6 +1,7 @@ import type { OID4VCICredentialFormat } from '@sphereon/oid4vci-common' import type { CredentialFormat } from '@sphereon/ssi-types' +import { AriesFrameworkError } from '@aries-framework/core' import { OpenId4VCIVersion } from '@sphereon/oid4vci-common' // Based on https://github.com/Sphereon-Opensource/OID4VCI/pull/54/files @@ -21,7 +22,7 @@ export function getUniformFormat(format: string | OID4VCICredentialFormat | Cred return 'ldp_vc' } - throw new Error(`Invalid format: ${format}`) + throw new AriesFrameworkError(`Invalid format: ${format}`) } export function getFormatForVersion(format: string, version: OpenId4VCIVersion) { diff --git a/packages/openid4vc-issuer/src/router/OpenId4VcIEndpointConfiguration.ts b/packages/openid4vc-issuer/src/router/OpenId4VcIEndpointConfiguration.ts new file mode 100644 index 0000000000..d197d15f9b --- /dev/null +++ b/packages/openid4vc-issuer/src/router/OpenId4VcIEndpointConfiguration.ts @@ -0,0 +1,119 @@ +import type { + IssuerMetadata, + CreateIssueCredentialResponseOptions, + MetadataEndpointConfig, + CredentialEndpointConfig, + AccessTokenEndpointConfig, +} from '../OpenId4VcIssuerServiceOptions' +import type { AgentContext, Logger } from '@aries-framework/core' +import type { + CNonceState, + CredentialIssuerMetadata, + CredentialOfferSession, + CredentialRequestV1_0_11, + CredentialResponse, + CredentialSupported, + IStateManager, +} from '@sphereon/oid4vci-common' +import type { Router, Request, Response } from 'express' + +import { handleTokenRequest, verifyTokenRequest } from './accessTokenEndpoint' +import { getEndpointMetadata, sendErrorResponse } from './utils' + +export interface InternalMetadataEndpointConfig extends MetadataEndpointConfig { + issuerMetadata: IssuerMetadata +} + +export function configureIssuerMetadataEndpoint( + router: Router, + logger: Logger, + config: InternalMetadataEndpointConfig +) { + const { issuerMetadata } = config + const wellKnownPath = `/.well-known/openid-credential-issuer` + + const { path, url } = getEndpointMetadata(wellKnownPath, issuerMetadata.credentialIssuer) + logger.info(`[OID4VCI] metadata hosted at '${url.toString()}'.`) + + const transformedMetadata: CredentialIssuerMetadata = { + credentials_supported: issuerMetadata.credentialsSupported as CredentialSupported[], + credential_endpoint: issuerMetadata.credentialEndpoint, + authorization_server: issuerMetadata.authorizationServer, + credential_issuer: issuerMetadata.credentialIssuer, + display: issuerMetadata.issuerDisplay ? [issuerMetadata.issuerDisplay] : undefined, + token_endpoint: issuerMetadata.tokenEndpoint, + } + + router.get(path, (_request: Request, response: Response) => { + response.status(200).json(transformedMetadata) + }) +} + +export interface InternalAccessTokenEndpointConfig extends AccessTokenEndpointConfig { + issuerMetadata: IssuerMetadata + cNonceExpiresIn: number + tokenExpiresIn: number + cNonceStateManager: IStateManager + credentialOfferSessionManager: IStateManager +} + +export function configureAccessTokenEndpoint( + agentContext: AgentContext, + router: Router, + logger: Logger, + config: InternalAccessTokenEndpointConfig +) { + const { issuerMetadata, credentialOfferSessionManager, preAuthorizedCodeExpirationDuration } = config + + const { path, url } = getEndpointMetadata(issuerMetadata.tokenEndpoint, issuerMetadata.credentialIssuer) + logger.info(`[OID4VCI] Token endpoint running at '${url.toString()}'.`) + + router.post( + path, + + verifyTokenRequest({ + logger, + credentialOfferSessionManager, + preAuthorizedCodeExpirationDuration, + }), + + handleTokenRequest(agentContext, logger, config) + ) +} + +export interface InternalCredentialEndpointConfig extends CredentialEndpointConfig { + issuerMetadata: IssuerMetadata + createIssueCredentialResponse: ( + agentContext: AgentContext, + options: CreateIssueCredentialResponseOptions + ) => Promise +} + +export function configureCredentialEndpoint( + agentContext: AgentContext, + router: Router, + logger: Logger, + config: InternalCredentialEndpointConfig +): void { + const { issuerMetadata, credentialRequestToCredentialMapper, verificationMethod } = config + + const { path, url } = getEndpointMetadata(issuerMetadata.credentialEndpoint, issuerMetadata.credentialIssuer) + logger.info(`[OID4VCI] Token endpoint running at '${url.toString()}'.`) + + router.post(path, async (request: Request, response: Response) => { + try { + const credentialRequest = request.body as CredentialRequestV1_0_11 + const credential = await credentialRequestToCredentialMapper(credentialRequest) + + const issueCredentialResponse = await config.createIssueCredentialResponse(agentContext, { + credentialRequest, + issuerMetadata, + verificationMethod, + credential, + }) + return response.send(issueCredentialResponse) + } catch (e) { + sendErrorResponse(response, logger, 500, 'invalid_request', e) + } + }) +} diff --git a/packages/openid4vc-issuer/src/router/accessTokenEndpoint.ts b/packages/openid4vc-issuer/src/router/accessTokenEndpoint.ts new file mode 100644 index 0000000000..5c6e3bcd45 --- /dev/null +++ b/packages/openid4vc-issuer/src/router/accessTokenEndpoint.ts @@ -0,0 +1,107 @@ +import type { InternalAccessTokenEndpointConfig } from './OpenId4VcIEndpointConfiguration' +import type { AgentContext, Logger, VerificationMethod, JwkJson } from '@aries-framework/core' +import type { CredentialOfferSession, IStateManager, JWTSignerCallback } from '@sphereon/oid4vci-common' +import type { NextFunction, Request, Response } from 'express' + +import { + AriesFrameworkError, + JwsService, + getJwkFromJson, + getKeyFromVerificationMethod, + JwtPayload, + getJwkClassFromKeyType, +} from '@aries-framework/core' +import { + GrantTypes, + PRE_AUTHORIZED_CODE_REQUIRED_ERROR, + TokenError, + TokenErrorResponse, +} from '@sphereon/oid4vci-common' +import { assertValidAccessTokenRequest, createAccessTokenResponse } from '@sphereon/oid4vci-issuer' + +import { sendErrorResponse } from './utils' + +const getJwtSignerCallback = ( + agentContext: AgentContext, + verificationMethod: VerificationMethod +): JWTSignerCallback => { + return async (jwt, _kid) => { + if (_kid) throw new AriesFrameworkError('Kid should not be supplied externally.') + + const jwsService = agentContext.dependencyManager.resolve(JwsService) + const key = getKeyFromVerificationMethod(verificationMethod) + + const alg = getJwkClassFromKeyType(key.keyType)?.supportedSignatureAlgorithms[0] + if (!alg) throw new AriesFrameworkError(`No supported signature algorithms for key type: ${key.keyType}`) + + const jwk = jwt.header.jwk ? getJwkFromJson(jwt.header.jwk as JwkJson) : undefined + + const signedJwt: string = await jwsService.createJwsCompact(agentContext, { + protectedHeaderOptions: { ...jwt.header, jwk, kid: verificationMethod.id, alg }, + payload: new JwtPayload(jwt.payload), + key, + }) + + return signedJwt + } +} + +export const handleTokenRequest = ( + agentContext: AgentContext, + logger: Logger, + config: InternalAccessTokenEndpointConfig +) => { + const { tokenExpiresIn, cNonceExpiresIn, interval } = config + + return async (request: Request, response: Response) => { + response.set({ 'Cache-Control': 'no-store', Pragma: 'no-cache' }) + + if (request.body.grant_type !== GrantTypes.PRE_AUTHORIZED_CODE) { + return response.status(400).json({ + error: TokenErrorResponse.invalid_request, + error_description: PRE_AUTHORIZED_CODE_REQUIRED_ERROR, + }) + } + + try { + const accessTokenResponse = await createAccessTokenResponse(request.body, { + credentialOfferSessions: config.credentialOfferSessionManager, + tokenExpiresIn, + accessTokenIssuer: config.issuerMetadata.credentialIssuer, + cNonce: await agentContext.wallet.generateNonce(), + cNonceExpiresIn, + cNonces: config.cNonceStateManager, + accessTokenSignerCallback: getJwtSignerCallback(agentContext, config.verificationMethod), + interval, + }) + return response.status(200).json(accessTokenResponse) + } catch (error) { + sendErrorResponse(response, logger, 400, TokenErrorResponse.invalid_request, error) + } + } +} + +export const verifyTokenRequest = (options: { + preAuthorizedCodeExpirationDuration: number + credentialOfferSessionManager: IStateManager + logger: Logger +}) => { + const { preAuthorizedCodeExpirationDuration, credentialOfferSessionManager, logger } = options + return async (request: Request, response: Response, next: NextFunction) => { + try { + await assertValidAccessTokenRequest(request.body, { + // we use seconds instead of milliseconds for consistency + expirationDuration: preAuthorizedCodeExpirationDuration * 1000, + credentialOfferSessions: credentialOfferSessionManager, + }) + } catch (error) { + if (error instanceof TokenError) { + sendErrorResponse(response, logger, error.statusCode, error.responseError + error.getDescription(), error) + } else { + sendErrorResponse(response, logger, 400, TokenErrorResponse.invalid_request, error) + } + } + + return next() + } +} diff --git a/packages/openid4vc-issuer/src/router/utils.ts b/packages/openid4vc-issuer/src/router/utils.ts new file mode 100644 index 0000000000..c3501bd3d6 --- /dev/null +++ b/packages/openid4vc-issuer/src/router/utils.ts @@ -0,0 +1,34 @@ +import type { Logger } from '@aries-framework/core' +import type { Response } from 'express' + +import { AriesFrameworkError } from '@aries-framework/core' + +export function sendErrorResponse(response: Response, logger: Logger, code: number, message: string, error: unknown) { + const error_description = + error instanceof Error ? error.message : typeof error === 'string' ? error : 'An unknown error occurred.' + + const body = { error: message, error_description } + logger.warn(`[OID4VCI] Sending error response: ${JSON.stringify(body)}`) + + return response.status(code).json(body) +} + +export function getEndpointMetadata(endpoint: string, base: string) { + const baseUrl = new URL(base) + + // if the endpoint is relative, append it to origin of the base + // if the endpoint is absolute, use the pathname + const url = new URL(endpoint, baseUrl) + + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + throw new AriesFrameworkError(`Endpoint '${endpoint}' is not valid. Invalid protocol '${url.protocol}'`) + } + + if (url.origin !== baseUrl.origin) { + throw new AriesFrameworkError(`Endpoint '${endpoint}' is not valid. Invalid origin '${url}'`) + } + + const path = url.pathname.replace(/\/$/, '') + + return { path, url } +} diff --git a/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts b/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts index b307482549..d14db6af88 100644 --- a/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts +++ b/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts @@ -2,7 +2,6 @@ import type { PreAuthorizedCodeFlowConfig, AuthorizationCodeFlowConfig, IssuerMetadata, - CredentialRequest, CredentialSupported, } from '../src/OpenId4VcIssuerServiceOptions' import type { @@ -12,6 +11,7 @@ import type { W3cVerifiableCredential, W3cVerifyCredentialResult, } from '@aries-framework/core' +import type { CredentialRequestV1_0_11 } from '@sphereon/oid4vci-common' import type { OriginalVerifiableCredential as SphereonW3cVerifiableCredential } from '@sphereon/ssi-types' import { AskarModule } from '@aries-framework/askar' @@ -88,7 +88,7 @@ const createCredentialRequestFromKid = async ( kid: string clientId?: string // use with the authorization code flow, } -): Promise => { +): Promise => { const { format, types, kid, nonce, issuerMetadata, clientId } = options const aud = issuerMetadata.credentialIssuer @@ -208,8 +208,8 @@ describe('OpenId4VcIssuer', () => { }) holderDid = holderDidCreateResult.didState.did as string - const holderDidKey = DidKey.fromDid(holderDidCreateResult.didState.did as string) - holderKid = `${holderDidCreateResult.didState.did as string}#${holderDidKey.key.fingerprint}` + const holderDidKey = DidKey.fromDid(holderDid) + holderKid = `${holderDid}#${holderDidKey.key.fingerprint}` const issuerDidCreateResult = await issuer.dids.create({ method: 'key', @@ -219,9 +219,9 @@ describe('OpenId4VcIssuer', () => { issuerDid = issuerDidCreateResult.didState.did as string - const verifierDidKey = DidKey.fromDid(issuerDid) - const verifierKid = `${issuerDidCreateResult.didState.did as string}#${verifierDidKey.key.fingerprint}` - const _issuerVerificationMethod = issuerDidCreateResult.didState.didDocument?.dereferenceKey(verifierKid, [ + const issuerDidKey = DidKey.fromDid(issuerDid) + const issuerKid = `${issuerDid}#${issuerDidKey.key.fingerprint}` + const _issuerVerificationMethod = issuerDidCreateResult.didState.didDocument?.dereferenceKey(issuerKid, [ 'authentication', ]) if (!_issuerVerificationMethod) throw new Error('No verification method found') From 21d265f3e9319a02ff3bdd5cf511fa40dc759c5b Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Thu, 23 Nov 2023 14:52:22 +0100 Subject: [PATCH 074/115] fix: consistency between issuer and verifier endpoints approach --- .../tests/openid4vci-holder.e2e.test.ts | 619 +++++++++--------- .../tests/openid4vp-holder.e2e.test.ts | 64 +- .../src/OpenId4VcVerifierApi.ts | 14 +- .../src/OpenId4VcVerifierModule.ts | 32 +- .../src/OpenId4VcVerifierModuleConfig.ts | 15 - .../src/OpenId4VcVerifierService.ts | 41 ++ .../src/OpenId4VcVerifierServiceOptions.ts | 17 + 7 files changed, 419 insertions(+), 383 deletions(-) diff --git a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts index 2ebfe8a1ba..e74987d93b 100644 --- a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts @@ -373,355 +373,362 @@ describe('OpenId4VcHolder', () => { ).rejects.toThrow() }) - describe('[DRAFT 11]: Pre-authorized flow', () => { - afterEach(() => { - cleanAll() - enableNetConnect() - }) - - it('[DRAFT 11]: Should successfully execute the pre-authorized if no credential is requested', async () => { - const fixture = waltIdJffJwt_draft_11 - - /** - * Below we're setting up some mock HTTP responses. - * These responses are based on the openid-initiate-issuance URI above - */ - // setup server metadata response - nock('https://jff.walt.id/issuer-api/default/oidc') - .get('/.well-known/openid-credential-issuer') - .reply(200, fixture.getMetadataResponse) - .get('/.well-known/openid-configuration') - .reply(404) - .get('/.well-known/oauth-authorization-server') - .reply(404) - - const resolvedCredentialOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer( - fixture.credentialOffer - ) - - const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( - resolvedCredentialOffer, - { - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], - proofOfPossessionVerificationMethodResolver: () => holderVerificationMethod, - verifyCredentialStatus: false, - credentialsToRequest: [], - } - ) - - expect(w3cCredentialRecords).toHaveLength(0) - }) - - it('[DRAFT 11]: Should successfully execute the pre-authorized flow using a single offered credential a did:key ES256 subject and JwtVc format', async () => { - const fixture = waltIdJffJwt_draft_11 - const httpMock = nock('https://jff.walt.id/issuer-api/default/oidc') - .get('/.well-known/openid-credential-issuer') - .reply(200, fixture.getMetadataResponse) - .get('/.well-known/openid-configuration') - .reply(404) - .get('/.well-known/oauth-authorization-server') - .reply(404) - - // setup access token response - httpMock.post('/token').reply(200, fixture.acquireAccessTokenResponse) - // setup credential request response - httpMock.post('/credential').reply(200, fixture.credentialResponse) + describe('[DRAFT 11]: Pre-authorized flow', () => { + afterEach(() => { + cleanAll() + enableNetConnect() + }) - const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer(fixture.credentialOffer) - expect(resolved.credentialsToRequest).toHaveLength(2) + it('[DRAFT 11]: Should successfully execute the pre-authorized if no credential is requested', async () => { + const fixture = waltIdJffJwt_draft_11 + + /** + * Below we're setting up some mock HTTP responses. + * These responses are based on the openid-initiate-issuance URI above + */ + // setup server metadata response + nock('https://jff.walt.id/issuer-api/default/oidc') + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.getMetadataResponse) + .get('/.well-known/openid-configuration') + .reply(404) + .get('/.well-known/oauth-authorization-server') + .reply(404) + + const resolvedCredentialOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer( + fixture.credentialOffer + ) - const selectedCredentialsForRequest = resolved.credentialsToRequest.filter((credential) => { - return ( - credential.format === OpenIdCredentialFormatProfile.JwtVcJson && credential.types.includes('VerifiableId') + const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + resolvedCredentialOffer, + { + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], + proofOfPossessionVerificationMethodResolver: () => holderVerificationMethod, + verifyCredentialStatus: false, + credentialsToRequest: [], + } ) + + expect(w3cCredentialRecords).toHaveLength(0) }) - expect(selectedCredentialsForRequest).toHaveLength(1) + it('[DRAFT 11]: Should successfully execute the pre-authorized flow using a single offered credential a did:key ES256 subject and JwtVc format', async () => { + const fixture = waltIdJffJwt_draft_11 + const httpMock = nock('https://jff.walt.id/issuer-api/default/oidc') + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.getMetadataResponse) + .get('/.well-known/openid-configuration') + .reply(404) + .get('/.well-known/oauth-authorization-server') + .reply(404) - const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( - resolved, - { - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], - proofOfPossessionVerificationMethodResolver: () => holderP256VerificationMethod, - verifyCredentialStatus: false, - credentialsToRequest: selectedCredentialsForRequest, - } - ) + // setup access token response + httpMock.post('/token').reply(200, fixture.acquireAccessTokenResponse) + // setup credential request response + httpMock.post('/credential').reply(200, fixture.credentialResponse) - expect(w3cCredentialRecords).toHaveLength(1) - expect(w3cCredentialRecords[0]).toBeInstanceOf(W3cCredentialRecord) - const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord + const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer(fixture.credentialOffer) + expect(resolved.credentialsToRequest).toHaveLength(2) - expect(w3cCredentialRecord.credential.type).toEqual([ - 'VerifiableCredential', - 'VerifiableAttestation', - 'VerifiableId', - ]) + const selectedCredentialsForRequest = resolved.credentialsToRequest.filter((credential) => { + return ( + credential.format === OpenIdCredentialFormatProfile.JwtVcJson && credential.types.includes('VerifiableId') + ) + }) - expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(holderP256Did) - }) + expect(selectedCredentialsForRequest).toHaveLength(1) - xit('[DRAFT 11]: Should successfully execute the pre-authorized flow using a single offered credential a did:key EdDSA subject and JsonLd format', async () => { - const fixture = waltIdJffJwt_draft_11 - const httpMock = nock('https://jff.walt.id/issuer-api/default/oidc') - .get('/.well-known/openid-credential-issuer') - .reply(200, fixture.getMetadataResponse) - .get('/.well-known/openid-configuration') - .reply(404) - .get('/.well-known/oauth-authorization-server') - .reply(404) + const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + resolved, + { + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], + proofOfPossessionVerificationMethodResolver: () => holderP256VerificationMethod, + verifyCredentialStatus: false, + credentialsToRequest: selectedCredentialsForRequest, + } + ) - // setup access token response - httpMock.post('/token').reply(200, fixture.acquireAccessTokenResponse) - // setup credential request response - httpMock.post('/credential').reply(200, fixture.jsonLdCredentialResponse) + expect(w3cCredentialRecords).toHaveLength(1) + expect(w3cCredentialRecords[0]).toBeInstanceOf(W3cCredentialRecord) + const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord - const resolvedCredentialOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer( - fixture.credentialOffer - ) + expect(w3cCredentialRecord.credential.type).toEqual([ + 'VerifiableCredential', + 'VerifiableAttestation', + 'VerifiableId', + ]) - expect(resolvedCredentialOffer.credentialsToRequest).toHaveLength(2) - const selectedCredentialsForRequest = resolvedCredentialOffer.credentialsToRequest.filter((credential) => { - return ( - credential.format === OpenIdCredentialFormatProfile.LdpVc && credential.types.includes('VerifiableDiploma') - ) + expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(holderP256Did) }) - expect(selectedCredentialsForRequest).toHaveLength(1) + xit('[DRAFT 11]: Should successfully execute the pre-authorized flow using a single offered credential a did:key EdDSA subject and JsonLd format', async () => { + const fixture = waltIdJffJwt_draft_11 + const httpMock = nock('https://jff.walt.id/issuer-api/default/oidc') + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.getMetadataResponse) + .get('/.well-known/openid-configuration') + .reply(404) + .get('/.well-known/oauth-authorization-server') + .reply(404) - const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( - resolvedCredentialOffer, - { - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - proofOfPossessionVerificationMethodResolver: () => holderVerificationMethod, - verifyCredentialStatus: false, - credentialsToRequest: selectedCredentialsForRequest, - } - ) - - expect(w3cCredentialRecords).toHaveLength(1) - expect(w3cCredentialRecords[0]).toBeInstanceOf(W3cCredentialRecord) - const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord - - expect(w3cCredentialRecord.credential.type).toEqual(['VerifiableCredential', 'PermanentResidentCard']) + // setup access token response + httpMock.post('/token').reply(200, fixture.acquireAccessTokenResponse) + // setup credential request response + httpMock.post('/credential').reply(200, fixture.jsonLdCredentialResponse) - expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(holderDid) - }) + const resolvedCredentialOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer( + fixture.credentialOffer + ) - xit('[DRAFT 11]: Should successfully execute the pre-authorized for multiple credentials of different formats using a did:key EdDsa subject', async () => { - const fixture = waltIdJffJwt_draft_11 - const httpMock = nock('https://jff.walt.id/issuer-api/default/oidc') - .get('/.well-known/openid-credential-issuer') - .reply(200, fixture.getMetadataResponse) - .get('/.well-known/openid-configuration') - .reply(404) - .get('/.well-known/oauth-authorization-server') - .reply(404) + expect(resolvedCredentialOffer.credentialsToRequest).toHaveLength(2) + const selectedCredentialsForRequest = resolvedCredentialOffer.credentialsToRequest.filter((credential) => { + return ( + credential.format === OpenIdCredentialFormatProfile.LdpVc && credential.types.includes('VerifiableDiploma') + ) + }) - // setup access token response - httpMock.post('/token').reply(200, fixture.credentialResponse) - // setup credential request response - httpMock.post('/credential').reply(200, fixture.credentialResponse) - httpMock.post('/credential').reply(200, fixture.jsonLdCredentialResponse) + expect(selectedCredentialsForRequest).toHaveLength(1) - const resolvedCredentialOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer( - fixture.credentialOffer - ) + const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + resolvedCredentialOffer, + { + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + proofOfPossessionVerificationMethodResolver: () => holderVerificationMethod, + verifyCredentialStatus: false, + credentialsToRequest: selectedCredentialsForRequest, + } + ) - expect(resolvedCredentialOffer.credentialsToRequest).toHaveLength(2) + expect(w3cCredentialRecords).toHaveLength(1) + expect(w3cCredentialRecords[0]).toBeInstanceOf(W3cCredentialRecord) + const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord - const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( - resolvedCredentialOffer, - { - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - proofOfPossessionVerificationMethodResolver: () => holderVerificationMethod, - verifyCredentialStatus: false, - } - ) + expect(w3cCredentialRecord.credential.type).toEqual(['VerifiableCredential', 'PermanentResidentCard']) - expect(w3cCredentialRecords.length).toEqual(2) - expect(w3cCredentialRecords[0]).toBeInstanceOf(W3cCredentialRecord) - const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord - expect(w3cCredentialRecord.credential.claimFormat).toEqual(ClaimFormat.JwtVc) - expect(w3cCredentialRecord.credential.type).toEqual([ - 'VerifiableCredential', - 'VerifiableAttestation', - 'VerifiableId', - ]) + expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(holderDid) + }) - expect(w3cCredentialRecords[1]).toBeInstanceOf(W3cCredentialRecord) - const w3cCredentialRecord1 = w3cCredentialRecords[1] as W3cCredentialRecord - expect(w3cCredentialRecord1.credential.claimFormat).toEqual(ClaimFormat.LdpVc) - expect(w3cCredentialRecord1.credential.type).toEqual(['VerifiableCredential', 'PermanentResidentCard']) - expect(w3cCredentialRecord1.credential.credentialSubjectIds[0]).toEqual(holderDid) - }) + xit('[DRAFT 11]: Should successfully execute the pre-authorized for multiple credentials of different formats using a did:key EdDsa subject', async () => { + const fixture = waltIdJffJwt_draft_11 + const httpMock = nock('https://jff.walt.id/issuer-api/default/oidc') + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.getMetadataResponse) + .get('/.well-known/openid-configuration') + .reply(404) + .get('/.well-known/oauth-authorization-server') + .reply(404) - it('authorization code flow https://portal.walt.id/', async () => { - const fixture = waltIssuerPortalV11 - // setup temporary redirect mock - nock('https://issuer.portal.walt.id') - .get('/.well-known/openid-credential-issuer') - .reply(200, fixture.issuerMetadata) - .get('/.well-known/openid-configuration') - .reply(404) - .get('/.well-known/oauth-authorization-server') - .reply(404) - .post('/par') - .reply(200, fixture.par) // setup access token response - .post('/token') - .reply(200, fixture.acquireAccessTokenResponse) + httpMock.post('/token').reply(200, fixture.credentialResponse) // setup credential request response - .post('/credential') - .reply(200, fixture.credentialResponse) + httpMock.post('/credential').reply(200, fixture.credentialResponse) + httpMock.post('/credential').reply(200, fixture.jsonLdCredentialResponse) - .get('/.well-known/oauth-authorization-server') - .reply(404) + const resolvedCredentialOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer( + fixture.credentialOffer + ) - const credentialOffer = `openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.portal.walt.id%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22OpenBadgeCredential%22%5D%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fpurl.imsglobal.org%2Fspec%2Fob%2Fv3p0%2Fcontext.json%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22OpenBadgeCredential%22%5D%7D%7D%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22b0e16785-d722-42a5-a04f-4beab28e03ea%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJiMGUxNjc4NS1kNzIyLTQyYTUtYTA0Zi00YmVhYjI4ZTAzZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.ibEpHFaHFBLWyhEf4SotDQZBeh_FMrfncWapNox1Iv1kdQWQ2cLQeS1VrCyVmPsbx0tN2MAyDFG7DnAaq8MiAA%22%2C%22user_pin_required%22%3Afalse%7D%7D%7D` - const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) + expect(resolvedCredentialOffer.credentialsToRequest).toHaveLength(2) - const resolvedAuthorizationRequest = await holder.modules.openId4VcHolder.resolveAuthorizationRequest(resolved, { - clientId: 'test-client', - redirectUri: 'http://blank', - scope: ['openid', 'OpenBadgeCredential'], + const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + resolvedCredentialOffer, + { + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + proofOfPossessionVerificationMethodResolver: () => holderVerificationMethod, + verifyCredentialStatus: false, + } + ) + + expect(w3cCredentialRecords.length).toEqual(2) + expect(w3cCredentialRecords[0]).toBeInstanceOf(W3cCredentialRecord) + const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord + expect(w3cCredentialRecord.credential.claimFormat).toEqual(ClaimFormat.JwtVc) + expect(w3cCredentialRecord.credential.type).toEqual([ + 'VerifiableCredential', + 'VerifiableAttestation', + 'VerifiableId', + ]) + + expect(w3cCredentialRecords[1]).toBeInstanceOf(W3cCredentialRecord) + const w3cCredentialRecord1 = w3cCredentialRecords[1] as W3cCredentialRecord + expect(w3cCredentialRecord1.credential.claimFormat).toEqual(ClaimFormat.LdpVc) + expect(w3cCredentialRecord1.credential.type).toEqual(['VerifiableCredential', 'PermanentResidentCard']) + expect(w3cCredentialRecord1.credential.credentialSubjectIds[0]).toEqual(holderDid) }) - const code = - 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJiMGUxNjc4NS1kNzIyLTQyYTUtYTA0Zi00YmVhYjI4ZTAzZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.ibEpHFaHFBLWyhEf4SotDQZBeh_FMrfncWapNox1Iv1kdQWQ2cLQeS1VrCyVmPsbx0tN2MAyDFG7DnAaq8MiAA' + it('authorization code flow https://portal.walt.id/', async () => { + const fixture = waltIssuerPortalV11 + // setup temporary redirect mock + nock('https://issuer.portal.walt.id') + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.issuerMetadata) + .get('/.well-known/openid-configuration') + .reply(404) + .get('/.well-known/oauth-authorization-server') + .reply(404) + .post('/par') + .reply(200, fixture.par) + // setup access token response + .post('/token') + .reply(200, fixture.acquireAccessTokenResponse) + // setup credential request response + .post('/credential') + .reply(200, fixture.credentialResponse) + + .get('/.well-known/oauth-authorization-server') + .reply(404) + + const credentialOffer = `openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.portal.walt.id%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22OpenBadgeCredential%22%5D%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fpurl.imsglobal.org%2Fspec%2Fob%2Fv3p0%2Fcontext.json%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22OpenBadgeCredential%22%5D%7D%7D%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22b0e16785-d722-42a5-a04f-4beab28e03ea%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJiMGUxNjc4NS1kNzIyLTQyYTUtYTA0Zi00YmVhYjI4ZTAzZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.ibEpHFaHFBLWyhEf4SotDQZBeh_FMrfncWapNox1Iv1kdQWQ2cLQeS1VrCyVmPsbx0tN2MAyDFG7DnAaq8MiAA%22%2C%22user_pin_required%22%3Afalse%7D%7D%7D` + const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) + + const resolvedAuthorizationRequest = await holder.modules.openId4VcHolder.resolveAuthorizationRequest( + resolved, + { + clientId: 'test-client', + redirectUri: 'http://blank', + scope: ['openid', 'OpenBadgeCredential'], + } + ) - const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( - resolved, - resolvedAuthorizationRequest, - code, - { - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - proofOfPossessionVerificationMethodResolver: () => holderVerificationMethod, - verifyCredentialStatus: false, - } - ) + const code = + 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJiMGUxNjc4NS1kNzIyLTQyYTUtYTA0Zi00YmVhYjI4ZTAzZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.ibEpHFaHFBLWyhEf4SotDQZBeh_FMrfncWapNox1Iv1kdQWQ2cLQeS1VrCyVmPsbx0tN2MAyDFG7DnAaq8MiAA' - expect(w3cCredentialRecords).toHaveLength(1) - }) + const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( + resolved, + resolvedAuthorizationRequest, + code, + { + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + proofOfPossessionVerificationMethodResolver: () => holderVerificationMethod, + verifyCredentialStatus: false, + } + ) - it('e2e flow with issuer endpoints requesting multiple credentials', async () => { - const router = Router() - await issuer.modules.openId4VcIssuer.configureRouter(router, { - metadataEndpointConfig: { enabled: true }, - accessTokenEndpointConfig: { - enabled: true, - preAuthorizedCodeExpirationDuration: 50, - verificationMethod: issuerVerificationMethod, - }, - credentialEndpointConfig: { - enabled: true, - verificationMethod: issuerVerificationMethod, - credentialRequestToCredentialMapper: async (credentialRequest) => { - if (credentialRequest.format === 'jwt_vc_json' && credentialRequest.types.includes('OpenBadgeCredential')) { - return new W3cCredential({ - type: openBadgeCredential.types, - issuer: new W3cIssuer({ id: issuerDid }), - credentialSubject: new W3cCredentialSubject({ id: holderDid }), - issuanceDate: w3cDate(Date.now()), - }) - } - - if ( - credentialRequest.format === 'jwt_vc_json' && - credentialRequest.types.includes('UniversityDegreeCredential') - ) { - return new W3cCredential({ - type: universityDegreeCredential.types, - issuer: new W3cIssuer({ id: issuerDid }), - credentialSubject: new W3cCredentialSubject({ id: holderDid }), - issuanceDate: w3cDate(Date.now()), - }) - } - throw new Error('Invalid request') - }, - }, + expect(w3cCredentialRecords).toHaveLength(1) }) - issuerApp.use('/', router) - issuerServer = issuerApp.listen(issuerPort) + it('e2e flow with issuer endpoints requesting multiple credentials', async () => { + const router = Router() + await issuer.modules.openId4VcIssuer.configureRouter(router, { + metadataEndpointConfig: { enabled: true }, + accessTokenEndpointConfig: { + enabled: true, + preAuthorizedCodeExpirationDuration: 50, + verificationMethod: issuerVerificationMethod, + }, + credentialEndpointConfig: { + enabled: true, + verificationMethod: issuerVerificationMethod, + credentialRequestToCredentialMapper: async (credentialRequest) => { + if ( + credentialRequest.format === 'jwt_vc_json' && + credentialRequest.types.includes('OpenBadgeCredential') + ) { + return new W3cCredential({ + type: openBadgeCredential.types, + issuer: new W3cIssuer({ id: issuerDid }), + credentialSubject: new W3cCredentialSubject({ id: holderDid }), + issuanceDate: w3cDate(Date.now()), + }) + } + + if ( + credentialRequest.format === 'jwt_vc_json' && + credentialRequest.types.includes('UniversityDegreeCredential') + ) { + return new W3cCredential({ + type: universityDegreeCredential.types, + issuer: new W3cIssuer({ id: issuerDid }), + credentialSubject: new W3cCredentialSubject({ id: holderDid }), + issuanceDate: w3cDate(Date.now()), + }) + } + throw new Error('Invalid request') + }, + }, + }) - const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { - preAuthorizedCode: '123456789', - userPinRequired: false, - } + issuerApp.use('/', router) + issuerServer = issuerApp.listen(issuerPort) - const { credentialOfferRequest } = await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest( - [ - openBadgeCredential.id, - { - format: universityDegreeCredential.format, - types: universityDegreeCredential.types, - }, - ], - { - preAuthorizedCodeFlowConfig, - ...baseCredentialRequestOptions, - baseUri: '', + const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { + preAuthorizedCode: '123456789', + userPinRequired: false, } - ) - const resolvedCredentialOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer( - credentialOfferRequest - ) + const { credentialOfferRequest } = await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest( + [ + openBadgeCredential.id, + { + format: universityDegreeCredential.format, + types: universityDegreeCredential.types, + }, + ], + { + preAuthorizedCodeFlowConfig, + ...baseCredentialRequestOptions, + baseUri: '', + } + ) - const credentials = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( - resolvedCredentialOffer, - { - proofOfPossessionVerificationMethodResolver: async () => { - return holderVerificationMethod - }, - } - ) + const resolvedCredentialOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer( + credentialOfferRequest + ) + + const credentials = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + resolvedCredentialOffer, + { + proofOfPossessionVerificationMethodResolver: async () => { + return holderVerificationMethod + }, + } + ) - expect(credentials).toHaveLength(2) - expect(credentials[0]).toBeInstanceOf(W3cCredentialRecord) - expect(credentials[0].credential.type).toHaveLength(2) - expect(credentials[1].credential.type).toHaveLength(2) - - if (credentials[0].credential.type.includes('OpenBadgeCredential')) { - expect(credentials[0].credential.type).toEqual(['VerifiableCredential', 'OpenBadgeCredential']) - expect(credentials[1].credential.type).toEqual(['VerifiableCredential', 'UniversityDegreeCredential']) - } else { - expect(credentials[1].credential.type).toEqual(['VerifiableCredential', 'OpenBadgeCredential']) - expect(credentials[0].credential.type).toEqual(['VerifiableCredential', 'UniversityDegreeCredential']) - } + expect(credentials).toHaveLength(2) + expect(credentials[0]).toBeInstanceOf(W3cCredentialRecord) + expect(credentials[0].credential.type).toHaveLength(2) + expect(credentials[1].credential.type).toHaveLength(2) + + if (credentials[0].credential.type.includes('OpenBadgeCredential')) { + expect(credentials[0].credential.type).toEqual(['VerifiableCredential', 'OpenBadgeCredential']) + expect(credentials[1].credential.type).toEqual(['VerifiableCredential', 'UniversityDegreeCredential']) + } else { + expect(credentials[1].credential.type).toEqual(['VerifiableCredential', 'OpenBadgeCredential']) + expect(credentials[0].credential.type).toEqual(['VerifiableCredential', 'UniversityDegreeCredential']) + } + }) }) - }) - //it('authorization code flow https://portal.walt.id/', async () => { - // const credentialOffer = `` - - // const didKey = DidKey.fromDid(did.didState.did as string) - // const kid = `${didKey.did}#${didKey.key.fingerprint}` - // const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) - // if (!verificationMethod) throw new Error('No verification method found') - - // const resolved = await agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) - - // const resolvedAuthorizationRequest = await agent.modules.openId4VcHolder.resolveAuthorizationRequest(resolved, { - // clientId: 'test-client', - // redirectUri: 'http://blank', - // }) - - // const code = - // 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJiMGUxNjc4NS1kNzIyLTQyYTUtYTA0Zi00YmVhYjI4ZTAzZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.ibEpHFaHFBLWyhEf4SotDQZBeh_FMrfncWapNox1Iv1kdQWQ2cLQeS1VrCyVmPsbx0tN2MAyDFG7DnAaq8MiAA' - - // const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( - // resolved, - // resolvedAuthorizationRequest, - // code, - // { - // allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - // proofOfPossessionVerificationMethodResolver: () => verificationMethod, - // verifyCredentialStatus: false, - // } - // ) - - // expect(w3cCredentialRecords).toHaveLength(1) - //}) + //it('authorization code flow https://portal.walt.id/', async () => { + // const credentialOffer = `` + + // const didKey = DidKey.fromDid(did.didState.did as string) + // const kid = `${didKey.did}#${didKey.key.fingerprint}` + // const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) + // if (!verificationMethod) throw new Error('No verification method found') + + // const resolved = await agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) + + // const resolvedAuthorizationRequest = await agent.modules.openId4VcHolder.resolveAuthorizationRequest(resolved, { + // clientId: 'test-client', + // redirectUri: 'http://blank', + // }) + + // const code = + // 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJiMGUxNjc4NS1kNzIyLTQyYTUtYTA0Zi00YmVhYjI4ZTAzZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.ibEpHFaHFBLWyhEf4SotDQZBeh_FMrfncWapNox1Iv1kdQWQ2cLQeS1VrCyVmPsbx0tN2MAyDFG7DnAaq8MiAA' + + // const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( + // resolved, + // resolvedAuthorizationRequest, + // code, + // { + // allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + // proofOfPossessionVerificationMethodResolver: () => verificationMethod, + // verifyCredentialStatus: false, + // } + // ) + + // expect(w3cCredentialRecords).toHaveLength(1) + //}) + }) }) diff --git a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts index aa39b2c9ba..21674d8f3d 100644 --- a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts @@ -9,7 +9,7 @@ import { Agent, DidKey, KeyType, TypedArrayEncoder, W3cJwtVerifiableCredential } import { agentDependencies } from '@aries-framework/node' import { OpenId4VcVerifierModule, SigningAlgo, staticOpOpenIdConfig } from '@aries-framework/openid4vc-verifier' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' -import express from 'express' +import express, { Router } from 'express' import nock from 'nock' import { OpenId4VcHolderModule } from '../src' @@ -83,6 +83,19 @@ const createHolderModules = () => { return modules } +const createVerifierModules = () => { + const modules = { + openId4VcVerifier: new OpenId4VcVerifierModule({}), + + askar: new AskarModule({ ariesAskar }), + } + + return modules +} + +type VerifierModules = ReturnType +type HolderModules = ReturnType + describe('OpenId4VcHolder | OpenID4VP', () => { let verifier: Agent let verifierVerificationMethod: VerificationMethod @@ -112,45 +125,26 @@ describe('OpenId4VcHolder | OpenID4VP', () => { }) } - const createVerifierModules = (verifierApp: Express) => { - const modules = { - openId4VcVerifier: new OpenId4VcVerifierModule({ - endpointConfig: { - app: verifierApp, - verificationEndpointPath, - proofResponseHandler: mockFunction, - }, - }), - - askar: new AskarModule({ ariesAskar }), - } - - return modules - } - - type VerifierModules = ReturnType - type HolderModules = ReturnType - beforeEach(async () => { verifierApp = express() verifier = new Agent({ config: { - label: 'OpenId4VcRp OpenID4VP Test39', + label: 'OpenId4VcRp OpenID4VP Test42', walletConfig: { - id: 'openid4vc-rp-openid4vp-test40', - key: 'openid4vc-rp-openid4vp-test41', + id: 'openid4vc-rp-openid4vp-test42', + key: 'openid4vc-rp-openid4vp-test42', }, }, dependencies: agentDependencies, - modules: createVerifierModules(verifierApp), + modules: createVerifierModules(), }) holder = new Agent({ config: { - label: 'OpenId4VcOp OpenID4VP Test39', + label: 'OpenId4VcOp OpenID4VP Test42', walletConfig: { - id: 'openid4vc-op-openid4vp-test40', - key: 'openid4vc-op-openid4vp-test41', + id: 'openid4vc-op-openid4vp-test42', + key: 'openid4vc-op-openid4vp-test42', }, }, dependencies: agentDependencies, @@ -160,8 +154,6 @@ describe('OpenId4VcHolder | OpenID4VP', () => { await verifier.initialize() await holder.initialize() - verifierServer = verifierApp.listen(port) - const verifierDid = await verifier.dids.create({ method: 'key', options: { keyType: KeyType.Ed25519 }, @@ -187,10 +179,22 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const _holderVerificationMethod = holderDid.didState.didDocument?.dereferenceKey(holderKid, ['authentication']) if (!_holderVerificationMethod) throw new Error('No verification method found') holderVerificationMethod = _holderVerificationMethod + + const router = await verifier.modules.openId4VcVerifier.configureRouter(Router(), { + verificationEndpointConfig: { + enabled: true, + verificationEndpointPath, + proofResponseHandler: mockFunction, + }, + }) + + verifierApp.use('/', router) + + verifierServer = verifierApp.listen(port) }) afterEach(async () => { - verifierServer.close() + verifierServer?.close() await holder.shutdown() await holder.wallet.delete() await verifier.shutdown() diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts index fd79e3c150..5d2c46b710 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts @@ -1,4 +1,5 @@ -import type { CreateProofRequestOptions, ProofPayload } from './OpenId4VcVerifierServiceOptions' +import type { CreateProofRequestOptions, EndpointConfig, ProofPayload } from './OpenId4VcVerifierServiceOptions' +import type { Router } from 'express' import { injectable, AgentContext } from '@aries-framework/core' @@ -49,4 +50,15 @@ export class OpenId4VcVerifierApi { public async verifyProofResponse(proofPayload: ProofPayload) { return await this.openId4VcVerifierService.verifyProofResponse(this.agentContext, proofPayload) } + + /** + * Configures the enabled endpoints for the given router, as specified in @link https://openid.net/specs/openid-4-verifiable-presentations-1_0.html + * + * @param router - The router to configure. + * @param endpointConfig - The endpoint configuration. + * @returns The configured router. + */ + public async configureRouter(router: Router, endpointConfig: EndpointConfig) { + return await this.openId4VcVerifierService.configureRouter(this.agentContext, router, endpointConfig) + } } diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierModule.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierModule.ts index 22d2e313ba..8a604c1f6c 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierModule.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierModule.ts @@ -1,9 +1,7 @@ import type { OpenId4VcVerifierModuleConfigOptions } from './OpenId4VcVerifierModuleConfig' -import type { AgentContext, DependencyManager, Module } from '@aries-framework/core' -import type { AuthorizationResponsePayload } from '@sphereon/did-auth-siop' +import type { DependencyManager, Module } from '@aries-framework/core' import { AgentConfig } from '@aries-framework/core' -import bodyParser from 'body-parser' import { OpenId4VcVerifierApi } from './OpenId4VcVerifierApi' import { OpenId4VcVerifierModuleConfig } from './OpenId4VcVerifierModuleConfig' @@ -41,32 +39,4 @@ export class OpenId4VcVerifierModule implements Module { // Services dependencyManager.registerSingleton(OpenId4VcVerifierService) } - - public async initialize(agentContext: AgentContext): Promise { - const endPointConfig = this.config.endpointConfig - if (!endPointConfig) return - - // create application/x-www-form-urlencoded parser - const urlencodedParser = bodyParser.urlencoded({ extended: false }) - - endPointConfig.app.post(endPointConfig.verificationEndpointPath, urlencodedParser, async (req, res, next) => { - try { - const isVpRequest = req.body.presentation_submission !== undefined - const verifierService = await agentContext.dependencyManager.resolve(OpenId4VcVerifierService) - - const authorizationResponse: AuthorizationResponsePayload = req.body - if (isVpRequest) authorizationResponse.presentation_submission = JSON.parse(req.body.presentation_submission) - - const verifiedProofResponse = await verifierService.verifyProofResponse(agentContext, req.body) - if (!endPointConfig.proofResponseHandler) return res.status(200).send() - - const { status } = await endPointConfig.proofResponseHandler(verifiedProofResponse) - return res.status(status).send() - } catch (error: unknown) { - next(error) - } - - return res.status(200).send() - }) - } } diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierModuleConfig.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierModuleConfig.ts index d9359d7b3b..d274c35bc5 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierModuleConfig.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierModuleConfig.ts @@ -1,18 +1,7 @@ import type { IInMemoryVerifierSessionManager } from './InMemoryVerifierSessionManager' -import type { VerifiedProofResponse } from './OpenId4VcVerifierServiceOptions' -import type { Express } from 'express' -export type ProofResponseHandlerReturn = { status: number } -export type ProofResponseHandler = (verifiedProofResponse: VerifiedProofResponse) => Promise - -export interface EndpointConfig { - app: Express - verificationEndpointPath: string - proofResponseHandler?: ProofResponseHandler -} export interface OpenId4VcVerifierModuleConfigOptions { sessionManager?: IInMemoryVerifierSessionManager - endpointConfig?: EndpointConfig } export class OpenId4VcVerifierModuleConfig { @@ -22,10 +11,6 @@ export class OpenId4VcVerifierModuleConfig { this.options = options } - public get endpointConfig() { - return this.options.endpointConfig - } - public get sessionManager() { return this.options.sessionManager } diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts index bee10828c6..68cc56b860 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts @@ -4,6 +4,7 @@ import type { CreateProofRequestOptions, ProofRequestMetadata, VerifiedProofResponse, + EndpointConfig, } from './OpenId4VcVerifierServiceOptions' import type { AgentContext, W3cVerifyPresentationResult } from '@aries-framework/core' import type { @@ -12,6 +13,7 @@ import type { PresentationVerificationCallback, SigningAlgo, } from '@sphereon/did-auth-siop' +import type { Router } from 'express' import { InjectionSymbols, @@ -37,6 +39,7 @@ import { VerificationMode, AuthorizationResponse, } from '@sphereon/did-auth-siop' +import bodyParser from 'body-parser' import { EventEmitter } from 'events' import { InMemoryVerifierSessionManager } from './InMemoryVerifierSessionManager' @@ -312,6 +315,44 @@ export class OpenId4VcVerifierService { return { verified: verificationResult.isValid } } } + + public configureRouter = (agentContext: AgentContext, router: Router, endpointConfig: EndpointConfig) => { + // parse application/x-www-form-urlencoded + router.use(bodyParser.urlencoded({ extended: false })) + + // parse application/json + router.use(bodyParser.json()) + + if (endpointConfig.verificationEndpointConfig?.enabled) { + router.post( + endpointConfig.verificationEndpointConfig.verificationEndpointPath, + async (request, response, next) => { + try { + const isVpRequest = request.body.presentation_submission !== undefined + const verifierService = await agentContext.dependencyManager.resolve(OpenId4VcVerifierService) + + const authorizationResponse: AuthorizationResponsePayload = request.body + if (isVpRequest) + authorizationResponse.presentation_submission = JSON.parse(request.body.presentation_submission) + + const verifiedProofResponse = await verifierService.verifyProofResponse(agentContext, request.body) + if (!endpointConfig.verificationEndpointConfig.proofResponseHandler) return response.status(200).send() + + const { status } = await endpointConfig.verificationEndpointConfig.proofResponseHandler( + verifiedProofResponse + ) + return response.status(status).send() + } catch (error: unknown) { + next(error) + } + + return response.status(200).send() + } + ) + } + + return router + } } async function generateRandomValues(agentContext: AgentContext, count: number) { diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts index b2d519ed76..dfaa7710e5 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts @@ -72,3 +72,20 @@ export const staticOpOpenIdConfig: HolderMetadata = { passBy: PassBy.VALUE, vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.ES256] }, jwt_vp: { alg: [SigningAlgo.ES256] } }, } + +export type ProofResponseHandlerReturn = { status: number } +export type ProofResponseHandler = (verifiedProofResponse: VerifiedProofResponse) => Promise + +export interface VerificationEndpointConfig { + /** + * Configures the router to expose the verification endpoint. + */ + enabled: boolean + + verificationEndpointPath: string + proofResponseHandler?: ProofResponseHandler +} + +export interface EndpointConfig { + verificationEndpointConfig: VerificationEndpointConfig +} From 0711848cbda6c420504bdf21c7773a10d9c5d981 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 27 Nov 2023 13:43:04 +0100 Subject: [PATCH 075/115] feat: add basic openid demo --- .eslintrc.js | 3 +- demo-openid/README.md | 89 +++++++++ demo-openid/package.json | 38 ++++ demo-openid/src/BaseAgent.ts | 61 ++++++ demo-openid/src/BaseInquirer.ts | 55 ++++++ demo-openid/src/Holder.ts | 102 ++++++++++ demo-openid/src/HolderInquirer.ts | 185 ++++++++++++++++++ demo-openid/src/Issuer.ts | 125 ++++++++++++ demo-openid/src/IssuerInquirer.ts | 89 +++++++++ demo-openid/src/OutputClass.ts | 40 ++++ demo-openid/src/Verifier.ts | 123 ++++++++++++ demo-openid/src/VerifierInquirer.ts | 90 +++++++++ demo-openid/tsconfig.json | 6 + package.json | 1 + packages/openid4vc-holder/src/issuance.ts | 2 +- packages/openid4vc-holder/src/presentation.ts | 2 + .../tests/openid4vci-holder.e2e.test.ts | 19 +- .../tests/openid4vp-holder.e2e.test.ts | 18 +- .../src/OpenId4VcIssuerApi.ts | 4 +- .../src/OpenId4VcIssuerService.ts | 14 +- .../src/OpenId4VcIssuerServiceOptions.ts | 11 +- packages/openid4vc-issuer/src/index.ts | 2 +- .../src/issuance/OpenId4VciHolderService.ts | 6 +- .../OpenId4VciHolderServiceOptions.ts | 3 +- .../src/issuance/utils/Formats.ts | 17 +- .../src/issuance/utils/IssuerMetadataUtils.ts | 6 +- .../router/OpenId4VcIEndpointConfiguration.ts | 18 +- .../presentation/OpenId4VpHolderService.ts | 4 +- .../OpenId4VpHolderServiceOptions.ts | 19 +- tsconfig.eslint.json | 1 + tsconfig.test.json | 2 +- yarn.lock | 2 +- 32 files changed, 1097 insertions(+), 60 deletions(-) create mode 100644 demo-openid/README.md create mode 100644 demo-openid/package.json create mode 100644 demo-openid/src/BaseAgent.ts create mode 100644 demo-openid/src/BaseInquirer.ts create mode 100644 demo-openid/src/Holder.ts create mode 100644 demo-openid/src/HolderInquirer.ts create mode 100644 demo-openid/src/Issuer.ts create mode 100644 demo-openid/src/IssuerInquirer.ts create mode 100644 demo-openid/src/OutputClass.ts create mode 100644 demo-openid/src/Verifier.ts create mode 100644 demo-openid/src/VerifierInquirer.ts create mode 100644 demo-openid/tsconfig.json diff --git a/.eslintrc.js b/.eslintrc.js index c669beed73..51535e4a4d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -99,7 +99,7 @@ module.exports = { }, }, { - files: ['demo/**'], + files: ['demo/**', 'demo-openid/**'], rules: { 'no-console': 'off', }, @@ -112,6 +112,7 @@ module.exports = { 'jest.*.ts', 'samples/**', 'demo/**', + 'demo-openid/**', 'scripts/**', '**/tests/**', ], diff --git a/demo-openid/README.md b/demo-openid/README.md new file mode 100644 index 0000000000..93f14ee99f --- /dev/null +++ b/demo-openid/README.md @@ -0,0 +1,89 @@ +

DEMO

+ +This is the Aries Framework Javascript demo. Walk through the AFJ flow yourself together with agents Alice and Faber. + +Alice, a former student of Faber College, connects with the College, is issued a credential about her degree and then is asked by the College for a proof. + +## Features + +- ✅ Creating a connection +- ✅ Offering a credential +- ✅ Requesting a proof +- ✅ Sending basic messages + +## Getting Started + +### Platform Specific Setup + +In order to use Aries Framework JavaScript some platform specific dependencies and setup is required. See our guides below to quickly set up you project with Aries Framework JavaScript for NodeJS, React Native and Electron. + +- [NodeJS](https://aries.js.org/guides/getting-started/installation/nodejs) + +### Run the demo + +These are the steps for running the AFJ demo: + +Clone the AFJ git repository: + +```sh +git clone https://github.com/hyperledger/aries-framework-javascript.git +``` + +Open two different terminals next to each other and in both, go to the demo folder: + +```sh +cd aries-framework-javascript/demo +``` + +Install the project in one of the terminals: + +```sh +yarn install +``` + +In the left terminal run Alice: + +```sh +yarn alice +``` + +In the right terminal run Faber: + +```sh +yarn faber +``` + +### Usage + +To set up a connection: + +- Select 'receive connection invitation' in Alice and 'create connection invitation' in Faber +- Faber will print a invitation link which you then copy and paste to Alice +- You have now set up a connection! + +To offer a credential: + +- Select 'offer credential' in Faber +- Faber will start with registering a schema and the credential definition accordingly +- You have now send a credential offer to Alice! +- Go to Alice to accept the incoming credential offer by selecting 'yes'. + +To request a proof: + +- Select 'request proof' in Faber +- Faber will create a new proof attribute and will then send a proof request to Alice! +- Go to Alice to accept the incoming proof request + +To send a basic message: + +- Select 'send message' in either one of the Agents +- Type your message and press enter +- Message sent! + +Exit: + +- Select 'exit' to shutdown the agent. + +Restart: + +- Select 'restart', to shutdown the current agent and start a new one diff --git a/demo-openid/package.json b/demo-openid/package.json new file mode 100644 index 0000000000..03812c9716 --- /dev/null +++ b/demo-openid/package.json @@ -0,0 +1,38 @@ +{ + "name": "afj-demo-openid", + "version": "1.0.0", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/hyperledger/aries-framework-javascript", + "directory": "demo-openid/" + }, + "license": "Apache-2.0", + "scripts": { + "issuer": "ts-node src/IssuerInquirer.ts", + "holder": "ts-node src/HolderInquirer.ts", + "verifier": "ts-node src/VerifierInquirer.ts", + "refresh": "rm -rf ./node_modules ./yarn.lock && yarn" + }, + "dependencies": { + "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.4", + "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", + "@hyperledger/indy-vdr-nodejs": "^0.2.0-dev.5", + "express": "^4.18.1", + "inquirer": "^8.2.5" + }, + "devDependencies": { + "@aries-framework/askar": "*", + "@aries-framework/core": "*", + "@aries-framework/node": "*", + "@aries-framework/openid4vc-holder": "*", + "@aries-framework/openid4vc-issuer": "*", + "@aries-framework/openid4vc-verifier": "*", + "@types/express": "^4.17.13", + "@types/figlet": "^1.5.4", + "@types/inquirer": "^8.2.6", + "clear": "^0.1.0", + "figlet": "^1.5.2", + "ts-node": "^10.4.0" + } +} diff --git a/demo-openid/src/BaseAgent.ts b/demo-openid/src/BaseAgent.ts new file mode 100644 index 0000000000..636a0d74d0 --- /dev/null +++ b/demo-openid/src/BaseAgent.ts @@ -0,0 +1,61 @@ +import type { InitConfig, KeyDidCreateOptions, ModulesMap, VerificationMethod } from '@aries-framework/core' +import type { Express } from 'express' + +import { Agent, DidKey, HttpOutboundTransport, KeyType, TypedArrayEncoder } from '@aries-framework/core' +import { HttpInboundTransport, agentDependencies } from '@aries-framework/node' +import express from 'express' + +import { greenText } from './OutputClass' + +export class BaseAgent { + public app: Express + public port: number + public name: string + public config: InitConfig + public agent: Agent + public did!: string + public didKey!: DidKey + public kid!: string + public verificationMethod!: VerificationMethod + + public constructor({ port, name, modules }: { port: number; name: string; modules: AgentModules }) { + this.name = name + this.port = port + this.app = express() + + const config = { + label: name, + walletConfig: { id: name, key: name }, + } satisfies InitConfig + + this.config = config + + this.agent = new Agent({ config, dependencies: agentDependencies, modules }) + + const httpInboundTransport = new HttpInboundTransport({ app: this.app, port: this.port }) + const httpOutboundTransport = new HttpOutboundTransport() + + this.agent.registerInboundTransport(httpInboundTransport) + this.agent.registerOutboundTransport(httpOutboundTransport) + } + + public async initializeAgent(secretPrivateKey: string) { + await this.agent.initialize() + + const didCreateResult = await this.agent.dids.create({ + method: 'key', + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: TypedArrayEncoder.fromString(secretPrivateKey) }, + }) + + this.did = didCreateResult.didState.did as string + this.didKey = DidKey.fromDid(this.did) + this.kid = `${this.did}#${this.didKey.key.fingerprint}` + + const verificationMethod = didCreateResult.didState.didDocument?.dereferenceKey(this.kid, ['authentication']) + if (!verificationMethod) throw new Error('No verification method found') + this.verificationMethod = verificationMethod + + console.log(greenText(`\nAgent ${this.name} created!\n`)) + } +} diff --git a/demo-openid/src/BaseInquirer.ts b/demo-openid/src/BaseInquirer.ts new file mode 100644 index 0000000000..358d72b632 --- /dev/null +++ b/demo-openid/src/BaseInquirer.ts @@ -0,0 +1,55 @@ +import { prompt } from 'inquirer' + +import { Title } from './OutputClass' + +export enum ConfirmOptions { + Yes = 'yes', + No = 'no', +} + +export class BaseInquirer { + public optionsInquirer: { type: string; prefix: string; name: string; message: string; choices: string[] } + public inputInquirer: { type: string; prefix: string; name: string; message: string; choices: string[] } + + public constructor() { + this.optionsInquirer = { + type: 'list', + prefix: '', + name: 'options', + message: '', + choices: [], + } + + this.inputInquirer = { + type: 'input', + prefix: '', + name: 'input', + message: '', + choices: [], + } + } + + public inquireOptions(promptOptions: string[]) { + this.optionsInquirer.message = Title.OptionsTitle + this.optionsInquirer.choices = promptOptions + return this.optionsInquirer + } + + public inquireInput(title: string) { + this.inputInquirer.message = title + return this.inputInquirer + } + + public inquireConfirmation(title: string) { + this.optionsInquirer.message = title + this.optionsInquirer.choices = [ConfirmOptions.Yes, ConfirmOptions.No] + return this.optionsInquirer + } + + public async inquireMessage() { + this.inputInquirer.message = Title.MessageTitle + const message = await prompt([this.inputInquirer]) + + return message.input[0] === 'q' ? null : message.input + } +} diff --git a/demo-openid/src/Holder.ts b/demo-openid/src/Holder.ts new file mode 100644 index 0000000000..05de66df93 --- /dev/null +++ b/demo-openid/src/Holder.ts @@ -0,0 +1,102 @@ +import type { W3cCredentialRecord } from '@aries-framework/core' +import type { + CredentialToRequest, + PresentationRequest, + PresentationSubmission, +} from '@aries-framework/openid4vc-holder' +import type { ResolvedCredentialOffer } from '@aries-framework/openid4vc-holder/build/issuance' +import type { ResolvedPresentationRequest } from '@aries-framework/openid4vc-holder/src/presentation' + +import { AskarModule } from '@aries-framework/askar' +import { OpenId4VcHolderModule } from '@aries-framework/openid4vc-holder' +import { ariesAskar } from '@hyperledger/aries-askar-nodejs' + +import { BaseAgent } from './BaseAgent' +import { Output } from './OutputClass' + +function getOpenIdHolderModules() { + return { + askar: new AskarModule({ ariesAskar }), + openId4VcHolder: new OpenId4VcHolderModule(), + } as const +} + +export class Holder extends BaseAgent> { + private presentationRequest?: PresentationRequest + private presentationSubmission?: PresentationSubmission + + public constructor(port: number, name: string) { + super({ port, name, modules: getOpenIdHolderModules() }) + } + + public static async build(): Promise { + const holder = new Holder(3000, 'OpenId4VcHolder ' + Math.random().toString()) + await holder.initializeAgent('96213c3d7fc8d4d6754c7a0fd969598e') + + return holder + } + + public async resolveCredentialOffer(credentialOffer: string) { + return await this.agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) + } + + public async requestAndStoreCredential( + resolvedCredentialOffer: ResolvedCredentialOffer, + credentialsToRequest: CredentialToRequest[] + ) { + const credentialRecords = await this.agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + resolvedCredentialOffer, + { + credentialsToRequest, + proofOfPossessionVerificationMethodResolver: async () => this.verificationMethod, + } + ) + + const storedCredentials: W3cCredentialRecord[] = await Promise.all( + credentialRecords.map(({ credential }) => this.agent.w3cCredentials.storeCredential({ credential })) + ) + + return storedCredentials + } + + public async resolveProofRequest(proofRequest: string) { + const resolvedProofRequest = await this.agent.modules.openId4VcHolder.resolveProofRequest(proofRequest) + + if (resolvedProofRequest.proofType === 'authentication') + throw new Error('We only support presentation requests for now.') + + return resolvedProofRequest + } + + public getProofRequestData() { + if (!this.presentationRequest || !this.presentationSubmission) + throw new Error('No proof request data set yet. You need to call resolveProofRequest first!') + return { + presentationRequest: this.presentationRequest, + presentationSubmission: this.presentationSubmission, + } + } + + public async acceptPresentationRequest( + resolvedPresentationReuest: ResolvedPresentationRequest, + submissionEntryIndexes: number[] + ) { + const { presentationRequest, presentationSubmission } = resolvedPresentationReuest + const submissionResult = await this.agent.modules.openId4VcHolder.acceptPresentationRequest(presentationRequest, { + submission: presentationSubmission, + submissionEntryIndexes, + }) + + return submissionResult.status + } + + public async exit() { + console.log(Output.Exit) + await this.agent.shutdown() + process.exit(0) + } + + public async restart() { + await this.agent.shutdown() + } +} diff --git a/demo-openid/src/HolderInquirer.ts b/demo-openid/src/HolderInquirer.ts new file mode 100644 index 0000000000..1dc9d89fd5 --- /dev/null +++ b/demo-openid/src/HolderInquirer.ts @@ -0,0 +1,185 @@ +import type { ResolvedCredentialOffer, ResolvedPresentationRequest } from '@aries-framework/openid4vc-holder' + +import { clear } from 'console' +import { textSync } from 'figlet' +import { prompt } from 'inquirer' + +import { BaseInquirer, ConfirmOptions } from './BaseInquirer' +import { Holder } from './Holder' +import { Title, greenText, redText } from './OutputClass' + +export const runHolder = async () => { + clear() + console.log(textSync('Holder', { horizontalLayout: 'full' })) + const holder = await HolderInquirer.build() + await holder.processAnswer() +} + +enum PromptOptions { + ResolveCredentialOffer = 'Resolve a credential offer.', + RequestCredential = 'Accept the credential offer.', + ResolveProofRequest = 'Resolve a proof request.', + AcceptPresentationRequest = 'Accept the presentation request.', + Exit = 'Exit', + Restart = 'Restart', +} + +export class HolderInquirer extends BaseInquirer { + public holder: Holder + public resolvedCredentialOffer?: ResolvedCredentialOffer + public resolvedPresentationRequest?: ResolvedPresentationRequest + + public constructor(holder: Holder) { + super() + this.holder = holder + } + + public static async build(): Promise { + const holder = await Holder.build() + return new HolderInquirer(holder) + } + + private async getPromptChoice() { + const promptOptions = [PromptOptions.ResolveCredentialOffer, PromptOptions.ResolveProofRequest] + + if (this.resolvedCredentialOffer) promptOptions.push(PromptOptions.RequestCredential) + if (this.resolvedPresentationRequest) promptOptions.push(PromptOptions.AcceptPresentationRequest) + + return prompt([this.inquireOptions(promptOptions.map((o) => o.valueOf()))]) + } + + public async processAnswer() { + const choice = await this.getPromptChoice() + + switch (choice.options) { + case PromptOptions.ResolveCredentialOffer: + await this.resolveCredentialOffer() + break + case PromptOptions.RequestCredential: + await this.requestCredential() + break + case PromptOptions.ResolveProofRequest: + await this.resolveProofRequest() + break + case PromptOptions.AcceptPresentationRequest: + await this.acceptPresentationRequest() + break + case PromptOptions.Exit: + await this.exit() + break + case PromptOptions.Restart: + await this.restart() + return + } + await this.processAnswer() + } + + public async exitUseCase(title: string) { + const confirm = await prompt([this.inquireConfirmation(title)]) + if (confirm.options === ConfirmOptions.No) { + return false + } else if (confirm.options === ConfirmOptions.Yes) { + return true + } + } + + public async resolveCredentialOffer() { + const credentialOffer = await prompt([this.inquireInput('Enter credential offer: ')]) + const resolvedCredentialOffer = await this.holder.resolveCredentialOffer(credentialOffer.input) + this.resolvedCredentialOffer = resolvedCredentialOffer + + console.log(greenText(`Received credential offer for the following credentials.`)) + console.log( + greenText( + resolvedCredentialOffer.credentialsToRequest.map((credential) => credential.types.join(', ')).join('\n') + ) + ) + } + + public async requestCredential() { + if (!this.resolvedCredentialOffer) { + throw new Error('No credential offer resolved yet.') + } + + const credentialsThatCanBeRequested = this.resolvedCredentialOffer.credentialsToRequest.map((credential) => + credential.types.join(', ') + ) + + const choice = await prompt([this.inquireOptions(credentialsThatCanBeRequested)]) + + const credentialToRequest = this.resolvedCredentialOffer.credentialsToRequest.find( + (credential) => credential.types.join(', ') == choice.options + ) + if (!credentialToRequest) throw new Error('Credential to request not found.') + + console.log(greenText(`Requesting the following credential '${credentialToRequest.types.join(', ')}'`)) + + const credentials = await this.holder.requestAndStoreCredential( + this.resolvedCredentialOffer, + this.resolvedCredentialOffer.credentialsToRequest + ) + + console.log(greenText(`Received and stored the following credentials.`)) + console.log(greenText(credentials.map((credential) => credential.credential.type.join(', ')).join('\n'))) + } + + public async resolveProofRequest() { + const proofRequestUri = await prompt([this.inquireInput('Enter proof request: ')]) + this.resolvedPresentationRequest = await this.holder.resolveProofRequest(proofRequestUri.input) + + const presentationDefinition = + this.resolvedPresentationRequest.presentationRequest.presentationDefinitions[0].definition + + console.log(greenText(`Presentation Purpose: '${presentationDefinition.purpose}'`)) + + if (this.resolvedPresentationRequest.presentationSubmission.areRequirementsSatisfied) { + console.log(greenText(`All requirements for creating the presentation are satisfied.`)) + } else { + console.log(redText(`No credentials available that satisfy the proof request.`)) + } + } + + public async acceptPresentationRequest() { + if (!this.resolvedPresentationRequest) throw new Error('No presentation request resolved yet.') + + // we know that only one credential is in the wallet and it satisfies the proof request. + // The submission entry index for this credential is 0. + const credential = + this.resolvedPresentationRequest.presentationSubmission.requirements[0].submissionEntry[0] + .verifiableCredentials[0] + const submissionEntryIndexes = [0] + + console.log(greenText(`Accepting the presentation request, with the following credential.`)) + console.log(greenText(credential.credential.type.join(', '))) + + const status = await this.holder.acceptPresentationRequest(this.resolvedPresentationRequest, submissionEntryIndexes) + + if (status >= 200 && status < 300) { + console.log(`received success status code '${status}'`) + } else { + console.log(`received error status code '${status}'`) + } + } + + public async exit() { + const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)]) + if (confirm.options === ConfirmOptions.No) { + return + } else if (confirm.options === ConfirmOptions.Yes) { + await this.holder.exit() + } + } + + public async restart() { + const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)]) + if (confirm.options === ConfirmOptions.No) { + await this.processAnswer() + return + } else if (confirm.options === ConfirmOptions.Yes) { + await this.holder.restart() + await runHolder() + } + } +} + +void runHolder() diff --git a/demo-openid/src/Issuer.ts b/demo-openid/src/Issuer.ts new file mode 100644 index 0000000000..1b0286df5f --- /dev/null +++ b/demo-openid/src/Issuer.ts @@ -0,0 +1,125 @@ +import type { + CredentialRequestToCredentialMapper, + CredentialSupported, + EndpointConfig, + OfferedCredential, +} from '@aries-framework/openid4vc-issuer' +import type e from 'express' + +import { AskarModule } from '@aries-framework/askar' +import { W3cCredential, W3cCredentialSubject, W3cIssuer, w3cDate } from '@aries-framework/core' +import { OpenId4VcIssuerModule } from '@aries-framework/openid4vc-issuer' +import { ariesAskar } from '@hyperledger/aries-askar-nodejs' +import { Router } from 'express' + +import { BaseAgent } from './BaseAgent' +import { Output } from './OutputClass' + +export const universityDegreeCredential: CredentialSupported & { id: string } = { + id: 'UniversityDegreeCredential', + format: 'jwt_vc_json', + types: ['VerifiableCredential', 'UniversityDegreeCredential'], +} + +export const openBadgeCredential: CredentialSupported & { id: string } = { + id: 'OpenBadgeCredential', + format: 'jwt_vc_json', + types: ['VerifiableCredential', 'OpenBadgeCredential'], +} + +export const credentialsSupported = [universityDegreeCredential, openBadgeCredential] + +function getOpenIdIssuerModules() { + return { + askar: new AskarModule({ ariesAskar }), + openId4VcIssuer: new OpenId4VcIssuerModule({ + issuerMetadata: { + credentialIssuer: 'http://localhost:2000', + tokenEndpoint: 'http://localhost:2000/token', + credentialEndpoint: 'http://localhost:2000/credentials', + credentialsSupported, + }, + }), + } as const +} + +export class Issuer extends BaseAgent> { + public constructor(port: number, name: string) { + super({ port, name, modules: getOpenIdIssuerModules() }) + } + + public static async build(): Promise { + const issuer = new Issuer(2000, 'OpenId4VcIssuer ' + Math.random().toString()) + await issuer.initializeAgent('96213c3d7fc8d4d6754c7a0fd969598g') + + return issuer + } + + public async configureRouter(): Promise { + const endpointConfig: EndpointConfig = { + metadataEndpointConfig: { enabled: true }, + accessTokenEndpointConfig: { + enabled: true, + verificationMethod: this.verificationMethod, + preAuthorizedCodeExpirationDuration: 100, + }, + credentialEndpointConfig: { + enabled: true, + verificationMethod: this.verificationMethod, + credentialRequestToCredentialMapper: await this.getCredentialRequestToCredentialMapper(), + }, + } + + const router = await this.agent.modules.openId4VcIssuer.configureRouter(Router(), endpointConfig) + this.app.use('/', router) + return router + } + + public getCredentialRequestToCredentialMapper(): CredentialRequestToCredentialMapper { + return async (credentialRequest, holderDid) => { + if ( + credentialRequest.format === 'jwt_vc_json' && + credentialRequest.types.includes('UniversityDegreeCredential') + ) { + return new W3cCredential({ + type: universityDegreeCredential.types, + issuer: new W3cIssuer({ id: this.did }), + credentialSubject: new W3cCredentialSubject({ id: holderDid }), + issuanceDate: w3cDate(Date.now()), + }) + } + + if (credentialRequest.format === 'jwt_vc_json' && credentialRequest.types.includes('OpenBadgeCredential')) { + return new W3cCredential({ + type: openBadgeCredential.types, + issuer: new W3cIssuer({ id: this.did }), + credentialSubject: new W3cCredentialSubject({ id: holderDid }), + issuanceDate: w3cDate(Date.now()), + }) + } + throw new Error('Invalid request') + } + } + + public async createCredentialOffer(offeredCredentials: OfferedCredential[]) { + const { credentialOfferRequest } = await this.agent.modules.openId4VcIssuer.createCredentialOfferAndRequest( + offeredCredentials, + { + scheme: 'openid-credential-offer', + preAuthorizedCodeFlowConfig: { userPinRequired: false }, + } + ) + + return credentialOfferRequest + } + + public async exit() { + console.log(Output.Exit) + await this.agent.shutdown() + process.exit(0) + } + + public async restart() { + await this.agent.shutdown() + } +} diff --git a/demo-openid/src/IssuerInquirer.ts b/demo-openid/src/IssuerInquirer.ts new file mode 100644 index 0000000000..a4167cfee2 --- /dev/null +++ b/demo-openid/src/IssuerInquirer.ts @@ -0,0 +1,89 @@ +import { clear } from 'console' +import { textSync } from 'figlet' +import { prompt } from 'inquirer' + +import { BaseInquirer, ConfirmOptions } from './BaseInquirer' +import { Issuer, credentialsSupported } from './Issuer' +import { Title, purpleText } from './OutputClass' + +export const runIssuer = async () => { + clear() + console.log(textSync('Issuer', { horizontalLayout: 'full' })) + const issuer = await IssuerInquirer.build() + await issuer.processAnswer() +} + +enum PromptOptions { + CreateCredentialOffer = 'Create a credential offer', + Exit = 'Exit', + Restart = 'Restart', +} + +export class IssuerInquirer extends BaseInquirer { + public issuer: Issuer + public promptOptionsString: string[] + + public constructor(issuer: Issuer) { + super() + this.issuer = issuer + this.promptOptionsString = Object.values(PromptOptions) + } + + public static async build(): Promise { + const issuer = await Issuer.build() + await issuer.configureRouter() + return new IssuerInquirer(issuer) + } + + private async getPromptChoice() { + return prompt([this.inquireOptions(this.promptOptionsString)]) + } + + public async processAnswer() { + const choice = await this.getPromptChoice() + + switch (choice.options) { + case PromptOptions.CreateCredentialOffer: + await this.createCredentialOffer() + break + case PromptOptions.Exit: + await this.exit() + break + case PromptOptions.Restart: + await this.restart() + return + } + await this.processAnswer() + } + + public async createCredentialOffer() { + const choice = await prompt([this.inquireOptions(credentialsSupported.map((credential) => credential.id))]) + const offeredCredential = credentialsSupported.find((credential) => credential.id === choice.options) + if (!offeredCredential) throw new Error(`No credential of type ${choice.options} found, that can be offered.`) + const offerRequest = await this.issuer.createCredentialOffer([offeredCredential]) + + console.log(purpleText(`credential offer request: '${offerRequest}'`)) + } + + public async exit() { + const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)]) + if (confirm.options === ConfirmOptions.No) { + return + } else if (confirm.options === ConfirmOptions.Yes) { + await this.issuer.exit() + } + } + + public async restart() { + const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)]) + if (confirm.options === ConfirmOptions.No) { + await this.processAnswer() + return + } else if (confirm.options === ConfirmOptions.Yes) { + await this.issuer.restart() + await runIssuer() + } + } +} + +void runIssuer() diff --git a/demo-openid/src/OutputClass.ts b/demo-openid/src/OutputClass.ts new file mode 100644 index 0000000000..b9e69c72f0 --- /dev/null +++ b/demo-openid/src/OutputClass.ts @@ -0,0 +1,40 @@ +export enum Color { + Green = `\x1b[32m`, + Red = `\x1b[31m`, + Purple = `\x1b[35m`, + Reset = `\x1b[0m`, +} + +export enum Output { + NoConnectionRecordFromOutOfBand = `\nNo connectionRecord has been created from invitation\n`, + ConnectionEstablished = `\nConnection established!`, + MissingConnectionRecord = `\nNo connectionRecord ID has been set yet\n`, + ConnectionLink = `\nRun 'Receive connection invitation' in Alice and paste this invitation link:\n\n`, + Exit = 'Shutting down agent...\nExiting...', +} + +export enum Title { + OptionsTitle = '\nOptions:', + InvitationTitle = '\n\nPaste the invitation url here:', + MessageTitle = '\n\nWrite your message here:\n(Press enter to send or press q to exit)\n', + ConfirmTitle = '\n\nAre you sure?', + CredentialOfferTitle = '\n\nCredential offer received, do you want to accept it?', + ProofRequestTitle = '\n\nProof request received, do you want to accept it?', +} + +export const greenText = (text: string, reset?: boolean) => { + if (reset) return Color.Green + text + Color.Reset + + return Color.Green + text +} + +export const purpleText = (text: string, reset?: boolean) => { + if (reset) return Color.Purple + text + Color.Reset + return Color.Purple + text +} + +export const redText = (text: string, reset?: boolean) => { + if (reset) return Color.Red + text + Color.Reset + + return Color.Red + text +} diff --git a/demo-openid/src/Verifier.ts b/demo-openid/src/Verifier.ts new file mode 100644 index 0000000000..7b99a5d5bf --- /dev/null +++ b/demo-openid/src/Verifier.ts @@ -0,0 +1,123 @@ +import type { + ProofResponseHandler, + CreateProofRequestOptions, + EndpointConfig, + PresentationDefinitionV2, +} from '@aries-framework/openid4vc-verifier' +import type e from 'express' + +import { AskarModule } from '@aries-framework/askar' +import { SigningAlgo } from '@aries-framework/openid4vc-verifier' +import { OpenId4VcVerifierModule } from '@aries-framework/openid4vc-verifier/src/OpenId4VcVerifierModule' +import { staticOpOpenIdConfig } from '@aries-framework/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions' +import { ariesAskar } from '@hyperledger/aries-askar-nodejs' +import { Router } from 'express' + +import { BaseAgent } from './BaseAgent' +import { Output } from './OutputClass' + +const universityDegreePresentationDefinition = { + id: 'UniversityDegreeCredential', + purpose: 'Present your UniversityDegreeCredential to verify your education level.', + input_descriptors: [ + { + id: 'UniversityDegreeCredential', + // changed jwt_vc_json to jwt_vc + format: { jwt_vc: { alg: ['EdDSA'] } }, + // changed $.type to $.vc.type + constraints: { + fields: [{ path: ['$.vc.type.*'], filter: { type: 'string', pattern: 'UniversityDegree' } }], + }, + }, + ], +} + +const openBadgeCredentialPresentationDefinition = { + id: 'OpenBadgeCredential', + purpose: 'Provide proof of employment to confirm your employment status.', + input_descriptors: [ + { + id: 'OpenBadgeCredential', + // changed jwt_vc_json to jwt_vc + format: { jwt_vc: { alg: ['EdDSA'] } }, + // changed $.type to $.vc.type + constraints: { + fields: [{ path: ['$.vc.type.*'], filter: { type: 'string', pattern: 'OpenBadgeCredential' } }], + }, + }, + ], +} + +export const presentationDefinitions = [ + universityDegreePresentationDefinition, + openBadgeCredentialPresentationDefinition, +] + +function getOpenIdVerifierModules() { + return { + askar: new AskarModule({ ariesAskar }), + openId4VcVerifier: new OpenId4VcVerifierModule({}), + } as const +} + +export class Verifier extends BaseAgent> { + private static verificationEndpointPath = '/verify' + + public constructor(port: number, name: string) { + super({ port, name, modules: getOpenIdVerifierModules() }) + } + + public static async build(): Promise { + const verifier = new Verifier(4000, 'OpenId4VcVerifier ' + Math.random().toString()) + await verifier.initializeAgent('96213c3d7fc8d4d6754c7a0fd969598f') + + return verifier + } + + public async configureVerifierRouter(): Promise { + const endpointConfig: EndpointConfig = { + verificationEndpointConfig: { + enabled: true, + verificationEndpointPath: Verifier.verificationEndpointPath, + proofResponseHandler: Verifier.proofResponseHandler, + }, + } + + const router = await this.agent.modules.openId4VcVerifier.configureRouter(Router(), endpointConfig) + this.app.use('/', router) + return router + } + + public async createProofRequest(presentationDefinition: PresentationDefinitionV2) { + const createProofRequestOptions: CreateProofRequestOptions = { + redirectUri: `http://localhost:${this.port}${Verifier.verificationEndpointPath}`, + verificationMethod: this.verificationMethod, + presentationDefinition, + holderMetadata: { + ...staticOpOpenIdConfig, + idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA], + requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA], + vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] }, jwt_vp: { alg: [SigningAlgo.EDDSA] } }, + }, + } + + const { proofRequest } = await this.agent.modules.openId4VcVerifier.createProofRequest(createProofRequestOptions) + + return proofRequest + } + + private static proofResponseHandler: ProofResponseHandler = async (payload) => { + console.log('Received a valid proof response', payload) + return { status: 200 } + } + + public async exit() { + console.log(Output.Exit) + await this.agent.shutdown() + process.exit(0) + } + + public async restart() { + await this.agent.shutdown() + } +} diff --git a/demo-openid/src/VerifierInquirer.ts b/demo-openid/src/VerifierInquirer.ts new file mode 100644 index 0000000000..46de1521cd --- /dev/null +++ b/demo-openid/src/VerifierInquirer.ts @@ -0,0 +1,90 @@ +import { clear } from 'console' +import { textSync } from 'figlet' +import { prompt } from 'inquirer' + +import { BaseInquirer, ConfirmOptions } from './BaseInquirer' +import { Title, purpleText } from './OutputClass' +import { Verifier, presentationDefinitions } from './Verifier' + +export const runVerifier = async () => { + clear() + console.log(textSync('Verifier', { horizontalLayout: 'full' })) + const verifier = await VerifierInquirer.build() + await verifier.processAnswer() +} + +enum PromptOptions { + CreateProofOffer = 'Request the presentation of a credential.', + Exit = 'Exit', + Restart = 'Restart', +} + +export class VerifierInquirer extends BaseInquirer { + public verifier: Verifier + public promptOptionsString: string[] + + public constructor(verifier: Verifier) { + super() + this.verifier = verifier + this.promptOptionsString = Object.values(PromptOptions) + } + + public static async build(): Promise { + const verifier = await Verifier.build() + await verifier.configureVerifierRouter() + return new VerifierInquirer(verifier) + } + + private async getPromptChoice() { + return prompt([this.inquireOptions(this.promptOptionsString)]) + } + + public async processAnswer() { + const choice = await this.getPromptChoice() + + switch (choice.options) { + case PromptOptions.CreateProofOffer: + await this.createProofRequest() + break + case PromptOptions.Exit: + await this.exit() + break + case PromptOptions.Restart: + await this.restart() + return + } + await this.processAnswer() + } + + public async createProofRequest() { + const choice = await prompt([this.inquireOptions(presentationDefinitions.map((p) => p.id))]) + const presentationDefinition = presentationDefinitions.find((p) => p.id === choice.options) + if (!presentationDefinition) throw new Error('No presentation definition found') + + const proofRequest = await this.verifier.createProofRequest(presentationDefinition) + + console.log(purpleText(`Proof request for the presentation of an ${choice.options}.\n'${proofRequest}'`)) + } + + public async exit() { + const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)]) + if (confirm.options === ConfirmOptions.No) { + return + } else if (confirm.options === ConfirmOptions.Yes) { + await this.verifier.exit() + } + } + + public async restart() { + const confirm = await prompt([this.inquireConfirmation(Title.ConfirmTitle)]) + if (confirm.options === ConfirmOptions.No) { + await this.processAnswer() + return + } else if (confirm.options === ConfirmOptions.Yes) { + await this.verifier.restart() + await runVerifier() + } + } +} + +void runVerifier() diff --git a/demo-openid/tsconfig.json b/demo-openid/tsconfig.json new file mode 100644 index 0000000000..b7d9de6c8e --- /dev/null +++ b/demo-openid/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "skipLibCheck": true + } +} diff --git a/package.json b/package.json index 3f7c1efbac..35a2e9366d 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "workspaces": [ "packages/*", "demo", + "demo-openid", "samples/*" ], "repository": { diff --git a/packages/openid4vc-holder/src/issuance.ts b/packages/openid4vc-holder/src/issuance.ts index 61ae53cfd9..1e347b5eb0 100644 --- a/packages/openid4vc-holder/src/issuance.ts +++ b/packages/openid4vc-holder/src/issuance.ts @@ -8,8 +8,8 @@ export type CredentialToRequest = Issuance.CredentialToRequest export type EndpointMetadataResult = Issuance.EndpointMetadataResult export type OfferedCredentialType = Issuance.OfferedCredentialType export type OpenId4VCIVersion = Issuance.OpenId4VCIVersion -export type OpenIdCredentialFormatProfile = Issuance.OpenIdCredentialFormatProfile export type ProofOfPossessionRequirements = Issuance.ProofOfPossessionRequirements +export type OpenIdCredentialFormatProfile = Issuance.OpenIdCredentialFormatProfile export type ProofOfPossessionVerificationMethodResolver = Issuance.ProofOfPossessionVerificationMethodResolver export type ProofOfPossessionVerificationMethodResolverOptions = Issuance.ProofOfPossessionVerificationMethodResolverOptions diff --git a/packages/openid4vc-holder/src/presentation.ts b/packages/openid4vc-holder/src/presentation.ts index 515ed9e525..ed8738db10 100644 --- a/packages/openid4vc-holder/src/presentation.ts +++ b/packages/openid4vc-holder/src/presentation.ts @@ -7,3 +7,5 @@ export type ProofSubmissionResponse = Presentation.ProofSubmissionResponse export type ResolvedProofRequest = Presentation.ResolvedProofRequest export type SubmissionEntry = Presentation.SubmissionEntry export type VpFormat = Presentation.VpFormat +export type ResolvedAuthenticationRequest = Presentation.ResolvedAuthenticationRequest +export type ResolvedPresentationRequest = Presentation.ResolvedPresentationRequest diff --git a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts index e74987d93b..a4c7d5748c 100644 --- a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts @@ -21,12 +21,11 @@ import { w3cDate, } from '@aries-framework/core' import { agentDependencies } from '@aries-framework/node' -import { OpenId4VcIssuerModule } from '@aries-framework/openid4vc-issuer' +import { OpenId4VcIssuerModule, OpenIdCredentialFormatProfile } from '@aries-framework/openid4vc-issuer' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import express, { Router, type Express } from 'express' import nock, { cleanAll, enableNetConnect } from 'nock' -import { OpenIdCredentialFormatProfile } from '../' import { OpenId4VcHolderModule } from '../src/OpenId4VcHolderModule' import { @@ -234,9 +233,7 @@ describe('OpenId4VcHolder', () => { // We only allow EdDSa, as we've created a did with keyType ed25519. If we create // or determine the did dynamically we could use any signature algorithm allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - credentialsToRequest: resolved.credentialsToRequest.filter( - (c) => c.format === OpenIdCredentialFormatProfile.LdpVc - ), + credentialsToRequest: resolved.credentialsToRequest.filter((c) => c.format === 'ldp_vc'), proofOfPossessionVerificationMethodResolver: () => holderVerificationMethod, } ) @@ -281,7 +278,7 @@ describe('OpenId4VcHolder', () => { proofOfPossessionVerificationMethodResolver: () => holderP256VerificationMethod, verifyCredentialStatus: false, credentialsToRequest: resolvedCredentialOffer.credentialsToRequest.filter((credential) => { - return credential.format === OpenIdCredentialFormatProfile.JwtVcJson + return credential.format === 'jwt_vc_json' }), } ) @@ -431,9 +428,7 @@ describe('OpenId4VcHolder', () => { expect(resolved.credentialsToRequest).toHaveLength(2) const selectedCredentialsForRequest = resolved.credentialsToRequest.filter((credential) => { - return ( - credential.format === OpenIdCredentialFormatProfile.JwtVcJson && credential.types.includes('VerifiableId') - ) + return credential.format === 'jwt_vc_json' && credential.types.includes('VerifiableId') }) expect(selectedCredentialsForRequest).toHaveLength(1) @@ -619,15 +614,17 @@ describe('OpenId4VcHolder', () => { credentialEndpointConfig: { enabled: true, verificationMethod: issuerVerificationMethod, - credentialRequestToCredentialMapper: async (credentialRequest) => { + credentialRequestToCredentialMapper: async (credentialRequest, _holderDid) => { if ( credentialRequest.format === 'jwt_vc_json' && credentialRequest.types.includes('OpenBadgeCredential') ) { + if (_holderDid !== holderDid) throw new Error('Invalid holder did') + return new W3cCredential({ type: openBadgeCredential.types, issuer: new W3cIssuer({ id: issuerDid }), - credentialSubject: new W3cCredentialSubject({ id: holderDid }), + credentialSubject: new W3cCredentialSubject({ id: _holderDid }), issuanceDate: w3cDate(Date.now()), }) } diff --git a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts index 21674d8f3d..9be2c12859 100644 --- a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts @@ -222,14 +222,14 @@ describe('OpenId4VcHolder | OpenID4VP', () => { //////////////////////////// OP (accept the verified request) //////////////////////////// const { submittedResponse, status } = await holder.modules.openId4VcHolder.acceptAuthenticationRequest( - result.request, + result.authenticationRequest, holderVerificationMethod ) expect(status).toBe(200) - expect(result.request.authorizationRequestPayload.redirect_uri).toBe(verificationEndpoint) - expect(result.request.issuer).toBe(verifierVerificationMethod.controller) + expect(result.authenticationRequest.authorizationRequestPayload.redirect_uri).toBe(verificationEndpoint) + expect(result.authenticationRequest.issuer).toBe(verifierVerificationMethod.controller) //////////////////////////// RP (verify the response) //////////////////////////// @@ -283,7 +283,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { //////////////////////////// OP (accept the verified request) //////////////////////////// const { submittedResponse, status } = await holder.modules.openId4VcHolder.acceptAuthenticationRequest( - result.request, + result.authenticationRequest, holderVerificationMethod ) @@ -379,7 +379,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { if (resolvedUniversityDegree.proofType !== 'presentation') throw new Error('expected prooftype presentation') await expect( - holder.modules.openId4VcHolder.acceptPresentationRequest(resolvedOpenBadge.request, { + holder.modules.openId4VcHolder.acceptPresentationRequest(resolvedOpenBadge.presentationRequest, { submission: resolvedUniversityDegree.presentationSubmission, submissionEntryIndexes: [0], }) @@ -409,7 +409,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const result = await holder.modules.openId4VcHolder.resolveProofRequest(proofRequest) if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') - const { request: presentationRequest, presentationSubmission } = result + const { presentationRequest, presentationSubmission } = result expect(presentationSubmission.areRequirementsSatisfied).toBeTruthy() expect(presentationSubmission.requirements.length).toBe(1) expect(presentationSubmission.requirements[0].needsCount).toBe(1) @@ -456,7 +456,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { expect(presentationSubmission.requirements[1].submissionEntry[0].inputDescriptorId).toBe('UniversityDegree') const { submittedResponse, status } = await holder.modules.openId4VcHolder.acceptPresentationRequest( - result.request, + result.presentationRequest, { submission: result.presentationSubmission, submissionEntryIndexes: [0, 0], @@ -530,7 +530,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') await expect( - holder.modules.openId4VcHolder.acceptPresentationRequest(result.request, { + holder.modules.openId4VcHolder.acceptPresentationRequest(result.presentationRequest, { submission: result.presentationSubmission, submissionEntryIndexes: [0], }) @@ -568,7 +568,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { //////////////////////////// OP (accept the verified request) //////////////////////////// const { submittedResponse, status } = await holder.modules.openId4VcHolder.acceptPresentationRequest( - result.request, + result.presentationRequest, { submission: result.presentationSubmission, submissionEntryIndexes: [0], diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts index a59be22cc6..aff712448d 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts @@ -35,8 +35,8 @@ export class OpenId4VcIssuerApi { * @param offeredCredentials - The credentials to be offered. * @param options.issuerMetadata - Metadata about the issuer. * @param options.credentialOfferUri - The URI to retrieve the credential offer if the offer is passed by reference. - * @param options.scheme - The credential offer request scheme. Default is https. - * @param options.baseUri - The base URI of the credential offer request. + * @param options.scheme - The credential offer request scheme. Default is 'https'. + * @param options.baseUri - The base URI of the credential offer request. Default is ''. * @param options.preAuthorizedCodeFlowConfig - The configuration for the pre-authorized code flow. This or the authorizationCodeFlowConfig must be provided. * @param options.authorizationCodeFlowConfig - The configuration for the authorization code flow. This or the preAuthorizedCodeFlowConfig must be provided. * diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts index 0b89de89e0..d8946b07c4 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts @@ -248,7 +248,8 @@ export class OpenId4VcIssuerService { return builder.build() } - private getGrantsFromConfig( + private async getGrantsFromConfig( + agentContext: AgentContext, preAuthorizedCodeFlowConfig?: PreAuthorizedCodeFlowConfig, authorizationCodeFlowConfig?: AuthorizationCodeFlowConfig ) { @@ -266,7 +267,8 @@ export class OpenId4VcIssuerService { /** * REQUIRED. The code representing the Credential Issuer's authorization for the Wallet to obtain Credentials of a certain type. */ - 'pre-authorized_code': preAuthorizedCodeFlowConfig.preAuthorizedCode, + 'pre-authorized_code': + preAuthorizedCodeFlowConfig.preAuthorizedCode ?? (await agentContext.wallet.generateNonce()), /** * OPTIONAL. Boolean value specifying whether the Credential Issuer expects presentation of a user PIN along with the Token Request * in a Pre-Authorized Code Flow. Default is false. @@ -279,7 +281,9 @@ export class OpenId4VcIssuerService { if (authorizationCodeFlowConfig) { grants = { ...grants, - authorization_code: { issuer_state: authorizationCodeFlowConfig.issuerState }, + authorization_code: { + issuer_state: authorizationCodeFlowConfig.issuerState ?? (await agentContext.wallet.generateNonce()), + }, } } @@ -339,11 +343,11 @@ export class OpenId4VcIssuerService { const vcIssuer = this.getVcIssuer(agentContext, issuerMetadata) const { uri, session } = await vcIssuer.createCredentialOfferURI({ - grants: this.getGrantsFromConfig(preAuthorizedCodeFlowConfig, authorizationCodeFlowConfig), + grants: await this.getGrantsFromConfig(agentContext, preAuthorizedCodeFlowConfig, authorizationCodeFlowConfig), credentials: offeredCredentials, credentialOfferUri: options.credentialOfferUri, scheme: options.scheme ?? 'https', - baseUri: options.baseUri, + baseUri: options.baseUri ?? '', // TODO: THIS IS WRONG HOW TO SPECIFY ldp_ creds? // credentialDefinition, }) diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts index 0e83189516..473b60b1bb 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts @@ -26,12 +26,12 @@ export interface CredentialSupported extends CommonCredentialSupported { export type OfferedCredential = CredentialOfferFormat | string export type PreAuthorizedCodeFlowConfig = { - preAuthorizedCode: string + preAuthorizedCode?: string userPinRequired?: boolean } export type AuthorizationCodeFlowConfig = { - issuerState: string + issuerState?: string } export type IssuerMetadata = { @@ -50,7 +50,7 @@ export interface CreateCredentialOfferAndRequestOptions { scheme?: 'http' | 'https' | 'openid-credential-offer' | string // The base URI of the credential offer uri - baseUri: string + baseUri?: string preAuthorizedCodeFlowConfig?: PreAuthorizedCodeFlowConfig authorizationCodeFlowConfig?: AuthorizationCodeFlowConfig @@ -72,6 +72,8 @@ export interface CreateIssueCredentialResponseOptions { issuerMetadata?: IssuerMetadata } +export { CredentialRequestV1_0_11 } + export { CredentialResponse } from '@sphereon/oid4vci-common' export interface MetadataEndpointConfig { @@ -105,7 +107,8 @@ export interface AccessTokenEndpointConfig { } export type CredentialRequestToCredentialMapper = ( - credentialRequest: CredentialRequestV1_0_11 + credentialRequest: CredentialRequestV1_0_11, + holderDid: string ) => Promise export interface CredentialEndpointConfig { diff --git a/packages/openid4vc-issuer/src/index.ts b/packages/openid4vc-issuer/src/index.ts index 18b7cbfcf1..eb4c37704b 100644 --- a/packages/openid4vc-issuer/src/index.ts +++ b/packages/openid4vc-issuer/src/index.ts @@ -2,7 +2,7 @@ import * as Issuance from './issuance' export { Issuance } -export { OpenId4VciHolderService } from './issuance' +export { OpenId4VciHolderService, OpenIdCredentialFormatProfile } from './issuance' export * from './OpenId4VcIssuerApi' export * from './OpenId4VcIssuerModule' diff --git a/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderService.ts b/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderService.ts index 3340c11991..39fdaa2765 100644 --- a/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderService.ts +++ b/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderService.ts @@ -229,8 +229,8 @@ export class OpenId4VciHolderService { opts?: { version?: OpenId4VCIVersion } ): Promise { let version = opts?.version ?? OpenId4VCIVersion.VER_1_0_11 - const claimedCredentialOfferUrl = `openid-credential-offer://?` - const claimedIssuanceInitiationUrl = `openid-initiate-issuance://?` + const claimedCredentialOfferUrl = `openid-credential-offer://` + const claimedIssuanceInitiationUrl = `openid-initiate-issuance://` if ( typeof credentialOffer === 'string' && @@ -728,7 +728,7 @@ export class OpenId4VciHolderService { this.logger.debug('Credential request response', credentialResponse) if (!credentialResponse.successBody) { - throw new AriesFrameworkError('Did not receive a successful credential response') + throw new AriesFrameworkError('Did not receive a successful credential response.') } const format = getUniformFormat(credentialResponse.successBody.format) as OpenIdCredentialFormatProfile diff --git a/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderServiceOptions.ts b/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderServiceOptions.ts index 81c32353a9..7212df7642 100644 --- a/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderServiceOptions.ts +++ b/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderServiceOptions.ts @@ -1,4 +1,5 @@ import type { OfferedCredentialType } from './utils/IssuerMetadataUtils' +import type { OpenIdCredentialFormatProfile } from './utils/claimFormatMapping' import type { JwaSignatureAlgorithm, KeyType, VerificationMethod } from '@aries-framework/core' import type { CredentialOfferPayloadV1_0_11, EndpointMetadataResult, OpenId4VCIVersion } from '@sphereon/oid4vci-common' @@ -6,7 +7,7 @@ export type SupportedCredentialFormats = 'jwt_vc_json' | 'jwt_vc_json-ld' export type { OfferedCredentialType, OpenId4VCIVersion, EndpointMetadataResult, CredentialOfferPayloadV1_0_11 } -export type CredentialToRequest = { format: string; types: string[] } & ( +export type CredentialToRequest = { format: OpenIdCredentialFormatProfile; types: string[] } & ( | { offerType: OfferedCredentialType.InlineCredentialOffer } | { offerType: OfferedCredentialType.CredentialSupported diff --git a/packages/openid4vc-issuer/src/issuance/utils/Formats.ts b/packages/openid4vc-issuer/src/issuance/utils/Formats.ts index 89cb688c86..67151fee37 100644 --- a/packages/openid4vc-issuer/src/issuance/utils/Formats.ts +++ b/packages/openid4vc-issuer/src/issuance/utils/Formats.ts @@ -1,25 +1,30 @@ -import type { OID4VCICredentialFormat } from '@sphereon/oid4vci-common' import type { CredentialFormat } from '@sphereon/ssi-types' import { AriesFrameworkError } from '@aries-framework/core' import { OpenId4VCIVersion } from '@sphereon/oid4vci-common' +import { OpenIdCredentialFormatProfile } from './claimFormatMapping' + // Based on https://github.com/Sphereon-Opensource/OID4VCI/pull/54/files -const isUniformFormat = (format: string): format is OID4VCICredentialFormat => { - return ['jwt_vc_json', 'jwt_vc_json-ld', 'ldp_vc'].includes(format) +// check if a string is a valid enum value of OpenIdCredentialFormatProfile + +const isUniformFormat = (format: string): format is OpenIdCredentialFormatProfile => { + return Object.values(OpenIdCredentialFormatProfile).includes(format as OpenIdCredentialFormatProfile) } -export function getUniformFormat(format: string | OID4VCICredentialFormat | CredentialFormat): OID4VCICredentialFormat { +export function getUniformFormat( + format: string | OpenIdCredentialFormatProfile | CredentialFormat +): OpenIdCredentialFormatProfile { // Already valid format if (isUniformFormat(format)) return format // Older formats if (format === 'jwt_vc' || format === 'jwt') { - return 'jwt_vc_json' + return OpenIdCredentialFormatProfile.JwtVcJson } if (format === 'ldp_vc' || format === 'ldp') { - return 'ldp_vc' + return OpenIdCredentialFormatProfile.LdpVc } throw new AriesFrameworkError(`Invalid format: ${format}`) diff --git a/packages/openid4vc-issuer/src/issuance/utils/IssuerMetadataUtils.ts b/packages/openid4vc-issuer/src/issuance/utils/IssuerMetadataUtils.ts index 1141e198af..63aec9bd60 100644 --- a/packages/openid4vc-issuer/src/issuance/utils/IssuerMetadataUtils.ts +++ b/packages/openid4vc-issuer/src/issuance/utils/IssuerMetadataUtils.ts @@ -1,3 +1,4 @@ +import type { OpenIdCredentialFormatProfile } from './claimFormatMapping' import type { AuthDetails } from '../OpenId4VciHolderService' import type { CredentialIssuerMetadata, @@ -8,7 +9,6 @@ import type { CredentialSupportedV1_0_08, EndpointMetadataResult, IssuerMetadataV1_0_08, - OID4VCICredentialFormat, } from '@sphereon/oid4vci-common' import { AriesFrameworkError } from '@aries-framework/core' @@ -31,13 +31,13 @@ export type OfferedCredentialWithMetadata = | { credentialSupported: CredentialSupported offerType: OfferedCredentialType.CredentialSupported - format: OID4VCICredentialFormat + format: OpenIdCredentialFormatProfile types: string[] } | { inlineCredentialOffer: CredentialOfferFormat offerType: OfferedCredentialType.InlineCredentialOffer - format: OID4VCICredentialFormat + format: OpenIdCredentialFormatProfile types: string[] } diff --git a/packages/openid4vc-issuer/src/router/OpenId4VcIEndpointConfiguration.ts b/packages/openid4vc-issuer/src/router/OpenId4VcIEndpointConfiguration.ts index d197d15f9b..c918733ab7 100644 --- a/packages/openid4vc-issuer/src/router/OpenId4VcIEndpointConfiguration.ts +++ b/packages/openid4vc-issuer/src/router/OpenId4VcIEndpointConfiguration.ts @@ -5,7 +5,6 @@ import type { CredentialEndpointConfig, AccessTokenEndpointConfig, } from '../OpenId4VcIssuerServiceOptions' -import type { AgentContext, Logger } from '@aries-framework/core' import type { CNonceState, CredentialIssuerMetadata, @@ -17,6 +16,8 @@ import type { } from '@sphereon/oid4vci-common' import type { Router, Request, Response } from 'express' +import { Jwt, type AgentContext, type Logger, DidsApi, AriesFrameworkError } from '@aries-framework/core' + import { handleTokenRequest, verifyTokenRequest } from './accessTokenEndpoint' import { getEndpointMetadata, sendErrorResponse } from './utils' @@ -103,7 +104,20 @@ export function configureCredentialEndpoint( router.post(path, async (request: Request, response: Response) => { try { const credentialRequest = request.body as CredentialRequestV1_0_11 - const credential = await credentialRequestToCredentialMapper(credentialRequest) + + if (!credentialRequest.proof?.jwt) throw new AriesFrameworkError('Received a credential request without a proof') + const jwt = Jwt.fromSerializedJwt(credentialRequest.proof?.jwt) + + const kid = jwt.header.kid + if (!kid) { + throw new AriesFrameworkError('Received a credential request without a kid') + } + + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const didDocument = await didsApi.resolveDidDocument(kid) + const holderDid = didDocument.id + + const credential = await credentialRequestToCredentialMapper(credentialRequest, holderDid) const issueCredentialResponse = await config.createIssueCredentialResponse(agentContext, { credentialRequest, diff --git a/packages/openid4vc-verifier/src/presentation/OpenId4VpHolderService.ts b/packages/openid4vc-verifier/src/presentation/OpenId4VpHolderService.ts index 9d374fe031..f392c85762 100644 --- a/packages/openid4vc-verifier/src/presentation/OpenId4VpHolderService.ts +++ b/packages/openid4vc-verifier/src/presentation/OpenId4VpHolderService.ts @@ -115,7 +115,7 @@ export class OpenId4VpHolderService { // which means you should never continue the authentication flow! const presentationDefs = verifiedAuthorizationRequest.presentationDefinitions if (!presentationDefs || presentationDefs.length === 0) { - return { proofType: 'authentication', request: verifiedAuthorizationRequest } + return { proofType: 'authentication', authenticationRequest: verifiedAuthorizationRequest } } // FIXME: I don't see any reason why we would support multiple presentation definitions @@ -133,7 +133,7 @@ export class OpenId4VpHolderService { presentationDefinition ) - return { proofType: 'presentation', request: verifiedAuthorizationRequest, presentationSubmission } + return { proofType: 'presentation', presentationRequest: verifiedAuthorizationRequest, presentationSubmission } } /** diff --git a/packages/openid4vc-verifier/src/presentation/OpenId4VpHolderServiceOptions.ts b/packages/openid4vc-verifier/src/presentation/OpenId4VpHolderServiceOptions.ts index 4a09fb0be1..60a756f758 100644 --- a/packages/openid4vc-verifier/src/presentation/OpenId4VpHolderServiceOptions.ts +++ b/packages/openid4vc-verifier/src/presentation/OpenId4VpHolderServiceOptions.ts @@ -14,13 +14,18 @@ export type PresentationRequest = VerifiedAuthorizationRequest & { presentationDefinitions: [PresentationDefinitionWithLocation] } -export type ResolvedProofRequest = - | { proofType: 'authentication'; request: AuthenticationRequest } - | { - proofType: 'presentation' - request: PresentationRequest - presentationSubmission: PresentationSubmission - } +export type ResolvedPresentationRequest = { + proofType: 'presentation' + presentationRequest: PresentationRequest + presentationSubmission: PresentationSubmission +} + +export type ResolvedAuthenticationRequest = { + proofType: 'authentication' + authenticationRequest: AuthenticationRequest +} + +export type ResolvedProofRequest = ResolvedAuthenticationRequest | ResolvedPresentationRequest export type ProofSubmissionResponse = { ok: boolean diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index 32670107d7..a8f908a24d 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -16,6 +16,7 @@ "tests", "samples", "demo", + "demo-openid", "scripts" ], "exclude": ["node_modules", "build"] diff --git a/tsconfig.test.json b/tsconfig.test.json index 096b728637..4fa873e5a5 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -10,6 +10,6 @@ }, "types": ["jest", "node"] }, - "include": ["tests", "samples", "demo", "packages/core/types/jest.d.ts"], + "include": ["tests", "samples", "demo", "demo-openid", "packages/core/types/jest.d.ts"], "exclude": ["node_modules", "build", "**/build/**"] } diff --git a/yarn.lock b/yarn.lock index eb3ac7833f..a15b71cfeb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5878,7 +5878,7 @@ expo-random@*: dependencies: base64-js "^1.3.0" -express@^4.17.1, express@^4.18.2: +express@^4.17.1, express@^4.18.1, express@^4.18.2: version "4.18.2" resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== From 037e68c2b187b74d14415508fa705b270e78fc00 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Fri, 1 Dec 2023 16:04:08 +0100 Subject: [PATCH 076/115] feat: SdJwtVc issuance (hacky) --- demo-openid/package.json | 1 + demo-openid/src/Holder.ts | 35 +- demo-openid/src/HolderInquirer.ts | 25 +- demo-openid/src/Issuer.ts | 52 ++- demo-openid/src/Verifier.ts | 2 +- packages/openid4vc-holder/package.json | 1 + .../src/OpenId4VcHolderApi.ts | 11 +- .../src/OpenId4VcHolderModule.ts | 2 +- packages/openid4vc-holder/src/issuance.ts | 4 +- .../tests/openid4vci-holder.e2e.test.ts | 97 ++++- packages/openid4vc-issuer/package.json | 4 +- .../src/OpenId4VcIssuerModule.ts | 2 +- .../src/OpenId4VcIssuerService.ts | 157 +++----- .../src/OpenId4VcIssuerServiceOptions.ts | 27 +- .../src/issuance/OpenId4VciHolderService.ts | 350 +++++++++--------- .../OpenId4VciHolderServiceOptions.ts | 37 +- .../src/issuance/utils/IssuerMetadataUtils.ts | 140 ++++--- .../__tests__/claimFormatMapping.test.ts | 4 - .../src/issuance/utils/claimFormatMapping.ts | 2 +- .../src/issuance/utils/index.ts | 1 + .../router/OpenId4VcIEndpointConfiguration.ts | 4 +- .../tests/openid4vc-issuer.e2e.test.ts | 211 +++++++---- packages/openid4vc-verifier/package.json | 1 + .../src/OpenId4VcVerifierModule.ts | 2 +- .../tests/openId4vc-verifier-module.test.ts | 2 +- packages/sd-jwt-vc/src/SdJwtVcApi.ts | 4 + packages/sd-jwt-vc/src/SdJwtVcService.ts | 47 +++ yarn.lock | 42 +-- 28 files changed, 756 insertions(+), 511 deletions(-) diff --git a/demo-openid/package.json b/demo-openid/package.json index 03812c9716..d028c947ca 100644 --- a/demo-openid/package.json +++ b/demo-openid/package.json @@ -28,6 +28,7 @@ "@aries-framework/openid4vc-holder": "*", "@aries-framework/openid4vc-issuer": "*", "@aries-framework/openid4vc-verifier": "*", + "@aries-framework/sd-jwt-vc": "^0.4.2", "@types/express": "^4.17.13", "@types/figlet": "^1.5.4", "@types/inquirer": "^8.2.6", diff --git a/demo-openid/src/Holder.ts b/demo-openid/src/Holder.ts index 05de66df93..ceeeede4b3 100644 --- a/demo-openid/src/Holder.ts +++ b/demo-openid/src/Holder.ts @@ -1,14 +1,13 @@ import type { W3cCredentialRecord } from '@aries-framework/core' import type { - CredentialToRequest, - PresentationRequest, - PresentationSubmission, + OfferedCredentialWithMetadata, + ResolvedPresentationRequest, + ResolvedCredentialOffer, } from '@aries-framework/openid4vc-holder' -import type { ResolvedCredentialOffer } from '@aries-framework/openid4vc-holder/build/issuance' -import type { ResolvedPresentationRequest } from '@aries-framework/openid4vc-holder/src/presentation' import { AskarModule } from '@aries-framework/askar' import { OpenId4VcHolderModule } from '@aries-framework/openid4vc-holder' +import { SdJwtVcModule, type SdJwtVcRecord } from '@aries-framework/sd-jwt-vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { BaseAgent } from './BaseAgent' @@ -18,13 +17,11 @@ function getOpenIdHolderModules() { return { askar: new AskarModule({ ariesAskar }), openId4VcHolder: new OpenId4VcHolderModule(), + sdJwtVc: new SdJwtVcModule(), } as const } export class Holder extends BaseAgent> { - private presentationRequest?: PresentationRequest - private presentationSubmission?: PresentationSubmission - public constructor(port: number, name: string) { super({ port, name, modules: getOpenIdHolderModules() }) } @@ -40,9 +37,9 @@ export class Holder extends BaseAgent> return await this.agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) } - public async requestAndStoreCredential( + public async requestAndStoreCredentials( resolvedCredentialOffer: ResolvedCredentialOffer, - credentialsToRequest: CredentialToRequest[] + credentialsToRequest: OfferedCredentialWithMetadata[] ) { const credentialRecords = await this.agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( resolvedCredentialOffer, @@ -52,8 +49,13 @@ export class Holder extends BaseAgent> } ) - const storedCredentials: W3cCredentialRecord[] = await Promise.all( - credentialRecords.map(({ credential }) => this.agent.w3cCredentials.storeCredential({ credential })) + const storedCredentials: (W3cCredentialRecord | SdJwtVcRecord)[] = await Promise.all( + credentialRecords.map((record) => { + if (record.type === 'W3cCredentialRecord') { + return this.agent.w3cCredentials.storeCredential({ credential: record.credential }) + } + return this.agent.modules.sdJwtVc.storeCredential2(record) + }) ) return storedCredentials @@ -68,15 +70,6 @@ export class Holder extends BaseAgent> return resolvedProofRequest } - public getProofRequestData() { - if (!this.presentationRequest || !this.presentationSubmission) - throw new Error('No proof request data set yet. You need to call resolveProofRequest first!') - return { - presentationRequest: this.presentationRequest, - presentationSubmission: this.presentationSubmission, - } - } - public async acceptPresentationRequest( resolvedPresentationReuest: ResolvedPresentationRequest, submissionEntryIndexes: number[] diff --git a/demo-openid/src/HolderInquirer.ts b/demo-openid/src/HolderInquirer.ts index 1dc9d89fd5..c9e009841f 100644 --- a/demo-openid/src/HolderInquirer.ts +++ b/demo-openid/src/HolderInquirer.ts @@ -1,5 +1,6 @@ import type { ResolvedCredentialOffer, ResolvedPresentationRequest } from '@aries-framework/openid4vc-holder' +import { OpenIdCredentialFormatProfile } from '@aries-framework/openid4vc-issuer' import { clear } from 'console' import { textSync } from 'figlet' import { prompt } from 'inquirer' @@ -90,9 +91,7 @@ export class HolderInquirer extends BaseInquirer { console.log(greenText(`Received credential offer for the following credentials.`)) console.log( - greenText( - resolvedCredentialOffer.credentialsToRequest.map((credential) => credential.types.join(', ')).join('\n') - ) + greenText(resolvedCredentialOffer.offeredCredentials.map((credential) => credential.types.join(', ')).join('\n')) ) } @@ -101,26 +100,36 @@ export class HolderInquirer extends BaseInquirer { throw new Error('No credential offer resolved yet.') } - const credentialsThatCanBeRequested = this.resolvedCredentialOffer.credentialsToRequest.map((credential) => + const credentialsThatCanBeRequested = this.resolvedCredentialOffer.offeredCredentials.map((credential) => credential.types.join(', ') ) const choice = await prompt([this.inquireOptions(credentialsThatCanBeRequested)]) - const credentialToRequest = this.resolvedCredentialOffer.credentialsToRequest.find( + const credentialToRequest = this.resolvedCredentialOffer.offeredCredentials.find( (credential) => credential.types.join(', ') == choice.options ) if (!credentialToRequest) throw new Error('Credential to request not found.') console.log(greenText(`Requesting the following credential '${credentialToRequest.types.join(', ')}'`)) - const credentials = await this.holder.requestAndStoreCredential( + const credentials = await this.holder.requestAndStoreCredentials( this.resolvedCredentialOffer, - this.resolvedCredentialOffer.credentialsToRequest + this.resolvedCredentialOffer.offeredCredentials ) console.log(greenText(`Received and stored the following credentials.`)) - console.log(greenText(credentials.map((credential) => credential.credential.type.join(', ')).join('\n'))) + console.log( + greenText( + credentials + .map((credential) => { + if (credential.type === 'W3cCredentialRecord') + return credential.credential.type.join(', ') + `, CredentialType: 'W3CVerifiableCredential'` + else return credential.sdJwtVc.payload.type + `, CredentialType: 'SdJwtVc'` + }) + .join('\n') + ) + ) } public async resolveProofRequest() { diff --git a/demo-openid/src/Issuer.ts b/demo-openid/src/Issuer.ts index 1b0286df5f..0a21c8fe94 100644 --- a/demo-openid/src/Issuer.ts +++ b/demo-openid/src/Issuer.ts @@ -8,29 +8,43 @@ import type e from 'express' import { AskarModule } from '@aries-framework/askar' import { W3cCredential, W3cCredentialSubject, W3cIssuer, w3cDate } from '@aries-framework/core' -import { OpenId4VcIssuerModule } from '@aries-framework/openid4vc-issuer' +import { OpenId4VcIssuerModule, OpenIdCredentialFormatProfile } from '@aries-framework/openid4vc-issuer' +import { SdJwtVcModule } from '@aries-framework/sd-jwt-vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { Router } from 'express' import { BaseAgent } from './BaseAgent' import { Output } from './OutputClass' -export const universityDegreeCredential: CredentialSupported & { id: string } = { +export const universityDegreeCredential = { id: 'UniversityDegreeCredential', - format: 'jwt_vc_json', + format: OpenIdCredentialFormatProfile.JwtVcJson, types: ['VerifiableCredential', 'UniversityDegreeCredential'], -} +} satisfies CredentialSupported & { id: string } -export const openBadgeCredential: CredentialSupported & { id: string } = { +export const openBadgeCredential = { id: 'OpenBadgeCredential', - format: 'jwt_vc_json', + format: OpenIdCredentialFormatProfile.JwtVcJson, types: ['VerifiableCredential', 'OpenBadgeCredential'], -} - -export const credentialsSupported = [universityDegreeCredential, openBadgeCredential] +} satisfies CredentialSupported & { id: string } + +export const universityDegreeCredentialSdJwt = { + id: 'UniversityDegreeCredential-sdjwt', + format: OpenIdCredentialFormatProfile.SdJwtVc, + credential_definition: { + vct: 'UniversityDegreeCredential', + }, +} satisfies CredentialSupported & { id: string } + +export const credentialsSupported = [ + universityDegreeCredential, + openBadgeCredential, + universityDegreeCredentialSdJwt, +] satisfies CredentialSupported[] function getOpenIdIssuerModules() { return { + sdJwtVc: new SdJwtVcModule(), askar: new AskarModule({ ariesAskar }), openId4VcIssuer: new OpenId4VcIssuerModule({ issuerMetadata: { @@ -50,7 +64,7 @@ export class Issuer extends BaseAgent> public static async build(): Promise { const issuer = new Issuer(2000, 'OpenId4VcIssuer ' + Math.random().toString()) - await issuer.initializeAgent('96213c3d7fc8d4d6754c7a0fd969598g') + await issuer.initializeAgent('96213c3d7fc8d4d6754c7a0fd969598f') return issuer } @@ -76,7 +90,7 @@ export class Issuer extends BaseAgent> } public getCredentialRequestToCredentialMapper(): CredentialRequestToCredentialMapper { - return async (credentialRequest, holderDid) => { + return async (credentialRequest, holderDid, holderKid) => { if ( credentialRequest.format === 'jwt_vc_json' && credentialRequest.types.includes('UniversityDegreeCredential') @@ -97,6 +111,22 @@ export class Issuer extends BaseAgent> issuanceDate: w3cDate(Date.now()), }) } + + if ( + credentialRequest.format === 'vc+sd-jwt' && + credentialRequest.credential_definition.vct === 'UniversityDegreeCredential' + ) { + const { compact } = await this.agent.modules.sdJwtVc.create( + { type: 'UniversityDegreeCredential', university: 'innsbruck', degree: 'bachelor' }, + { + holderDidUrl: holderKid, + issuerDidUrl: this.kid, + disclosureFrame: { university: true, degree: true }, + } + ) + return compact + } + throw new Error('Invalid request') } } diff --git a/demo-openid/src/Verifier.ts b/demo-openid/src/Verifier.ts index 7b99a5d5bf..3234fb80c8 100644 --- a/demo-openid/src/Verifier.ts +++ b/demo-openid/src/Verifier.ts @@ -69,7 +69,7 @@ export class Verifier extends BaseAgent { const verifier = new Verifier(4000, 'OpenId4VcVerifier ' + Math.random().toString()) - await verifier.initializeAgent('96213c3d7fc8d4d6754c7a0fd969598f') + await verifier.initializeAgent('96213c3d7fc8d4d6754c7a0fd969598g') return verifier } diff --git a/packages/openid4vc-holder/package.json b/packages/openid4vc-holder/package.json index 20cc19d8cd..38a3e1de35 100644 --- a/packages/openid4vc-holder/package.json +++ b/packages/openid4vc-holder/package.json @@ -26,6 +26,7 @@ "dependencies": { "@aries-framework/askar": "^0.4.2", "@aries-framework/core": "0.4.2", + "@aries-framework/sd-jwt-vc": "^0.4.2", "@aries-framework/openid4vc-verifier": "0.4.2", "@aries-framework/openid4vc-issuer": "0.4.2" }, diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts index 108ebcb006..d7e08fb571 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts @@ -7,6 +7,7 @@ import type { } from './issuance' import type { AuthenticationRequest, PresentationRequest, PresentationSubmission } from './presentation' import type { VerificationMethod, W3cCredentialRecord } from '@aries-framework/core' +import type { SdJwtVcRecord } from '@aries-framework/sd-jwt-vc' import { injectable, AgentContext } from '@aries-framework/core' import { OpenId4VciHolderService } from '@aries-framework/openid4vc-issuer' @@ -89,7 +90,7 @@ export class OpenId4VcHolderApi { * into a unified format. * * @param credentialOffer the credential offer to resolve - * @returns The uniform credential offer payload, the issuer metadata, protocol version, and credentials that can be requested. + * @returns The uniform credential offer payload, the issuer metadata, protocol version, and the offered credentials with metadata. */ public async resolveCredentialOffer(credentialOffer: string | CredentialOfferPayloadV1_0_11) { return await this.openId4VciHolderService.resolveCredentialOffer(credentialOffer) @@ -123,12 +124,12 @@ export class OpenId4VcHolderApi { * Accepts a credential offer using the pre-authorized code flow. * @param resolvedCredentialOffer Obtained through @see resolveCredentialOffer * @param acceptCredentialOfferOptions - * @returns W3cCredentialRecord[] + * @returns ( @see W3cCredentialRecord | @see SdJwtRecord )[] */ public async acceptCredentialOfferUsingPreAuthorizedCode( resolvedCredentialOffer: ResolvedCredentialOffer, acceptCredentialOfferOptions: AcceptCredentialOfferOptions - ): Promise { + ): Promise<(W3cCredentialRecord | SdJwtVcRecord)[]> { return this.openId4VciHolderService.acceptCredentialOffer(this.agentContext, { resolvedCredentialOffer, acceptCredentialOfferOptions, @@ -141,14 +142,14 @@ export class OpenId4VcHolderApi { * @param resolvedAuthorizationRequest Obtained through @see resolveAuthorizationRequest * @param code The authorization code obtained via the authorization request URI * @param acceptCredentialOfferOptions - * @returns W3cCredentialRecord[] + * @returns ( @see W3cCredentialRecord | @see SdJwtRecord )[] */ public async acceptCredentialOfferUsingAuthorizationCode( resolvedCredentialOffer: ResolvedCredentialOffer, resolvedAuthorizationRequest: ResolvedAuthorizationRequest, code: string, acceptCredentialOfferOptions: AcceptCredentialOfferOptions - ): Promise { + ): Promise<(W3cCredentialRecord | SdJwtVcRecord)[]> { return this.openId4VciHolderService.acceptCredentialOffer(this.agentContext, { resolvedCredentialOffer, resolvedAuthorizationRequestWithCode: { ...resolvedAuthorizationRequest, code }, diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts b/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts index fe63de5d06..5292a2b0d2 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts +++ b/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts @@ -21,7 +21,7 @@ export class OpenId4VcHolderModule implements Module { dependencyManager .resolve(AgentConfig) .logger.warn( - "The '@aries-framework/openid4vc-holder' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." + "The '@aries-framework/openid4vc-holder' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages. Multi-Tenancy is not supported." ) // Api diff --git a/packages/openid4vc-holder/src/issuance.ts b/packages/openid4vc-holder/src/issuance.ts index 1e347b5eb0..ab19cafa31 100644 --- a/packages/openid4vc-holder/src/issuance.ts +++ b/packages/openid4vc-holder/src/issuance.ts @@ -1,10 +1,10 @@ import type { Issuance } from '@aries-framework/openid4vc-issuer' export type AcceptCredentialOfferOptions = Issuance.AcceptCredentialOfferOptions +export type OfferedCredentialWithMetadata = Issuance.OfferedCredentialWithMetadata export type AuthCodeFlowOptions = Issuance.AuthCodeFlowOptions -export type AuthDetails = Issuance.AuthDetails +export type AuthDetails = Issuance.AuthorizationDetails export type CredentialOfferPayloadV1_0_11 = Issuance.CredentialOfferPayloadV1_0_11 -export type CredentialToRequest = Issuance.CredentialToRequest export type EndpointMetadataResult = Issuance.EndpointMetadataResult export type OfferedCredentialType = Issuance.OfferedCredentialType export type OpenId4VCIVersion = Issuance.OpenId4VCIVersion diff --git a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts index a4c7d5748c..08bbb5b204 100644 --- a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts @@ -22,6 +22,7 @@ import { } from '@aries-framework/core' import { agentDependencies } from '@aries-framework/node' import { OpenId4VcIssuerModule, OpenIdCredentialFormatProfile } from '@aries-framework/openid4vc-issuer' +import { SdJwtVcModule } from '@aries-framework/sd-jwt-vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import express, { Router, type Express } from 'express' import nock, { cleanAll, enableNetConnect } from 'nock' @@ -40,22 +41,31 @@ const credentialIssuer = `http://localhost:${issuerPort}` const openBadgeCredential: CredentialSupported & { id: string } = { id: `${credentialIssuer}/credentials/OpenBadgeCredential`, - format: 'jwt_vc_json', + format: OpenIdCredentialFormatProfile.JwtVcJson, types: ['VerifiableCredential', 'OpenBadgeCredential'], } const universityDegreeCredential: CredentialSupported & { id: string } = { id: `${credentialIssuer}/credentials/UniversityDegreeCredential`, - format: 'jwt_vc_json', + format: OpenIdCredentialFormatProfile.JwtVcJson, types: ['VerifiableCredential', 'UniversityDegreeCredential'], } const universityDegreeCredentialLd: CredentialSupported & { id: string } = { id: `${credentialIssuer}/credentials/UniversityDegreeCredentialLd`, - format: 'jwt_vc_json-ld', + format: OpenIdCredentialFormatProfile.JwtVcJsonLd, types: ['VerifiableCredential', 'UniversityDegreeCredential'], + '@context': ['context'], } +const universityDegreeCredentialSdJwt = { + id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredentialSdJwt', + format: OpenIdCredentialFormatProfile.SdJwtVc, + credential_definition: { + vct: 'UniversityDegreeCredential', + }, +} satisfies CredentialSupported & { id: string } + const baseCredentialRequestOptions = { scheme: 'openid-credential-offer', baseUri: credentialIssuer, @@ -65,16 +75,18 @@ const issuerMetadata: IssuerMetadata = { credentialIssuer, credentialEndpoint: `${credentialIssuer}/credentials`, tokenEndpoint: `${credentialIssuer}/token`, - credentialsSupported: [openBadgeCredential, universityDegreeCredentialLd], + credentialsSupported: [openBadgeCredential, universityDegreeCredentialLd, universityDegreeCredentialSdJwt], } const holderModules = { openId4VcHolder: new OpenId4VcHolderModule(), + sdJwtVc: new SdJwtVcModule(), askar: new AskarModule({ ariesAskar }), } const issuerModules = { openId4VcIssuer: new OpenId4VcIssuerModule({ issuerMetadata }), + sdJwtVc: new SdJwtVcModule(), askar: new AskarModule({ ariesAskar }), } @@ -233,7 +245,7 @@ describe('OpenId4VcHolder', () => { // We only allow EdDSa, as we've created a did with keyType ed25519. If we create // or determine the did dynamically we could use any signature algorithm allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - credentialsToRequest: resolved.credentialsToRequest.filter((c) => c.format === 'ldp_vc'), + credentialsToRequest: resolved.offeredCredentials.filter((c) => c.format === 'ldp_vc'), proofOfPossessionVerificationMethodResolver: () => holderVerificationMethod, } ) @@ -277,7 +289,7 @@ describe('OpenId4VcHolder', () => { allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], proofOfPossessionVerificationMethodResolver: () => holderP256VerificationMethod, verifyCredentialStatus: false, - credentialsToRequest: resolvedCredentialOffer.credentialsToRequest.filter((credential) => { + credentialsToRequest: resolvedCredentialOffer.offeredCredentials.filter((credential) => { return credential.format === 'jwt_vc_json' }), } @@ -425,9 +437,9 @@ describe('OpenId4VcHolder', () => { httpMock.post('/credential').reply(200, fixture.credentialResponse) const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer(fixture.credentialOffer) - expect(resolved.credentialsToRequest).toHaveLength(2) + expect(resolved.offeredCredentials).toHaveLength(2) - const selectedCredentialsForRequest = resolved.credentialsToRequest.filter((credential) => { + const selectedCredentialsForRequest = resolved.offeredCredentials.filter((credential) => { return credential.format === 'jwt_vc_json' && credential.types.includes('VerifiableId') }) @@ -475,8 +487,8 @@ describe('OpenId4VcHolder', () => { fixture.credentialOffer ) - expect(resolvedCredentialOffer.credentialsToRequest).toHaveLength(2) - const selectedCredentialsForRequest = resolvedCredentialOffer.credentialsToRequest.filter((credential) => { + expect(resolvedCredentialOffer.offeredCredentials).toHaveLength(2) + const selectedCredentialsForRequest = resolvedCredentialOffer.offeredCredentials.filter((credential) => { return ( credential.format === OpenIdCredentialFormatProfile.LdpVc && credential.types.includes('VerifiableDiploma') ) @@ -523,7 +535,7 @@ describe('OpenId4VcHolder', () => { fixture.credentialOffer ) - expect(resolvedCredentialOffer.credentialsToRequest).toHaveLength(2) + expect(resolvedCredentialOffer.offeredCredentials).toHaveLength(2) const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( resolvedCredentialOffer, @@ -683,6 +695,9 @@ describe('OpenId4VcHolder', () => { expect(credentials).toHaveLength(2) expect(credentials[0]).toBeInstanceOf(W3cCredentialRecord) + if (credentials[0].type === 'SdJwtVcRecord') throw new Error('Invalid credential type') + if (credentials[1].type === 'SdJwtVcRecord') throw new Error('Invalid credential type') + expect(credentials[0].credential.type).toHaveLength(2) expect(credentials[1].credential.type).toHaveLength(2) @@ -696,6 +711,66 @@ describe('OpenId4VcHolder', () => { }) }) + it('e2e flow with issuer endpoints requesting sdjwtvc', async () => { + const router = Router() + await issuer.modules.openId4VcIssuer.configureRouter(router, { + metadataEndpointConfig: { enabled: true }, + accessTokenEndpointConfig: { + enabled: true, + preAuthorizedCodeExpirationDuration: 50, + verificationMethod: issuerVerificationMethod, + }, + credentialEndpointConfig: { + enabled: true, + verificationMethod: issuerVerificationMethod, + credentialRequestToCredentialMapper: async (credentialRequest, _holderDid, holderKid) => { + if ( + credentialRequest.format === 'vc+sd-jwt' && + credentialRequest.credential_definition.vct === 'UniversityDegreeCredential' + ) { + if (_holderDid !== holderDid) throw new Error('Invalid holder did') + + const { compact } = await issuer.modules.sdJwtVc.create( + { type: 'UniversityDegreeCredential', university: 'innsbruck', degree: 'bachelor' }, + { + holderDidUrl: holderKid, + issuerDidUrl: issuerVerificationMethod.id, + disclosureFrame: { university: true, degree: true }, + } + ) + return compact + } + throw new Error('Invalid request') + }, + }, + }) + + issuerApp.use('/', router) + issuerServer = issuerApp.listen(issuerPort) + + const { credentialOfferRequest } = await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest( + [universityDegreeCredentialSdJwt.id], + { preAuthorizedCodeFlowConfig: { userPinRequired: false }, ...baseCredentialRequestOptions } + ) + + const resolvedCredentialOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer( + credentialOfferRequest + ) + + const credentials = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + resolvedCredentialOffer, + { + proofOfPossessionVerificationMethodResolver: async () => { + return holderVerificationMethod + }, + } + ) + + expect(credentials).toHaveLength(1) + if (credentials[0].type === 'W3cCredentialRecord') throw new Error('Invalid credential type') + expect(credentials[0].sdJwtVc.payload['type']).toEqual('UniversityDegreeCredential') + }) + //it('authorization code flow https://portal.walt.id/', async () => { // const credentialOffer = `` diff --git a/packages/openid4vc-issuer/package.json b/packages/openid4vc-issuer/package.json index 932f311ca4..fb0ac45ff7 100644 --- a/packages/openid4vc-issuer/package.json +++ b/packages/openid4vc-issuer/package.json @@ -26,9 +26,7 @@ "dependencies": { "@aries-framework/askar": "^0.4.2", "@aries-framework/core": "0.4.2", - "@sphereon/oid4vci-issuer": "^0.8.1", - "@sphereon/oid4vci-common": "^0.8.1", - "@sphereon/oid4vci-client": "^0.8.1", + "@aries-framework/sd-jwt-vc": "^0.4.2", "@sphereon/ssi-types": "^0.17.5", "body-parser": "^1.20.2" }, diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerModule.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerModule.ts index ab7daa8ec6..71e38a52ee 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerModule.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerModule.ts @@ -26,7 +26,7 @@ export class OpenId4VcIssuerModule implements Module { dependencyManager .resolve(AgentConfig) .logger.warn( - "The '@aries-framework/openid4vc-issuer' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." + "The '@aries-framework/openid4vc-issuer' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages. Multi-Tenancy is not supported." ) // Register config diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts index d8946b07c4..e4d27042ba 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts @@ -8,6 +8,7 @@ import type { CredentialOfferAndRequest, EndpointConfig, } from './OpenId4VcIssuerServiceOptions' +import type { OfferedCredentialWithMetadata } from './issuance/utils/IssuerMetadataUtils' import type { AgentContext, VerificationMethod, @@ -16,15 +17,11 @@ import type { JwaSignatureAlgorithm, } from '@aries-framework/core' import type { - CredentialRequestJwtVc, - CredentialRequestLdpVc, Grant, MetadataDisplay, JWTVerifyCallback, - CredentialOfferFormat, CredentialRequestV1_0_11, CredentialOfferPayloadV1_0_11, - CredentialSupported as SphereonCredentialSupported, } from '@sphereon/oid4vci-common' import type { CredentialDataSupplier, @@ -57,6 +54,8 @@ import { VcIssuerBuilder } from '@sphereon/oid4vci-issuer' import bodyParser from 'body-parser' import { OpenId4VcIssuerModuleConfig } from './OpenId4VcIssuerModuleConfig' +import { OpenIdCredentialFormatProfile } from './issuance' +import { getOfferedCredentialsWithMetadata } from './issuance/utils/IssuerMetadataUtils' import { configureAccessTokenEndpoint, configureCredentialEndpoint, @@ -170,7 +169,15 @@ export class OpenId4VcIssuerService { } } - private getCredentialSigningCallback = ( + private getSdJwtVcCredentialSigningCallback = (): CredentialSignerCallback => { + return async (opts) => { + const { credential } = opts + // TODO: sdjwt + return credential as any + } + } + + private getW3cCredentialSigningCallback = ( agentContext: AgentContext, issuerVerificationMethod: VerificationMethod ): CredentialSignerCallback => { @@ -179,24 +186,22 @@ export class OpenId4VcIssuerService { const { alg, kid, didDocument: holderDidDocument } = jwtVerifyResult - if (!kid) throw new AriesFrameworkError('No KID present for binding the credential to a holder.') - if (!holderDidDocument) { - throw new AriesFrameworkError('No DID document present for binding the credential to a holder.') - } + if (!kid) throw new AriesFrameworkError('Missing Kid. Cannot create the holder binding') + if (!holderDidDocument) throw new AriesFrameworkError('Missing did document. Cannot create the holder binding.') // If the Credential shall be bound to a DID, the kid refers to a DID URL which identifies a // particular key in the DID Document that the Credential shall be bound to. const holderVerificationMethod = holderDidDocument.dereferenceKey(kid, ['assertionMethod']) let signed: W3cVerifiableCredential - if (format === 'jwt_vc_json' || format === 'jwt_vc_json-ld') { + if (format === OpenIdCredentialFormatProfile.JwtVcJson || format === OpenIdCredentialFormatProfile.JwtVcJsonLd) { signed = await this.w3cCredentialService.signCredential(agentContext, { format: ClaimFormat.JwtVc, credential: W3cCredential.fromJson(credential), verificationMethod: issuerVerificationMethod.id, alg: alg as JwaSignatureAlgorithm, }) - } else { + } else if (format === OpenIdCredentialFormatProfile.LdpVc) { signed = await this.w3cCredentialService.signCredential(agentContext, { format: ClaimFormat.LdpVc, credential: W3cCredential.fromJson(credential), @@ -204,6 +209,8 @@ export class OpenId4VcIssuerService { proofPurpose: 'assertionMethod', proofType: this.getProofTypeForLdpVc(agentContext, holderVerificationMethod), }) + } else { + throw new AriesFrameworkError(`Unsupported credential format '${format}' for W3C credential signing callback.`) } return getSphereonW3cVerifiableCredential(signed) @@ -226,8 +233,7 @@ export class OpenId4VcIssuerService { .withCredentialIssuer(credentialIssuer) .withCredentialEndpoint(credentialEndpoint) .withTokenEndpoint(tokenEndpoint) - // FIXME: currently credentialsSupported is not typed correctly - .withCredentialsSupported(credentialsSupported as SphereonCredentialSupported[]) + .withCredentialsSupported(credentialsSupported) .withCNonceExpiresIn(this.openId4VcIssuerModuleConfig.cNonceExpiresIn) .withCNonceStateManager(this.cNonceStateManager) .withCredentialOfferStateManager(this.credentialOfferSessionManager) @@ -259,74 +265,21 @@ export class OpenId4VcIssuerService { ) } - let grants: Grant = {} - if (preAuthorizedCodeFlowConfig) { - grants = { - ...grants, - 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { - /** - * REQUIRED. The code representing the Credential Issuer's authorization for the Wallet to obtain Credentials of a certain type. - */ - 'pre-authorized_code': - preAuthorizedCodeFlowConfig.preAuthorizedCode ?? (await agentContext.wallet.generateNonce()), - /** - * OPTIONAL. Boolean value specifying whether the Credential Issuer expects presentation of a user PIN along with the Token Request - * in a Pre-Authorized Code Flow. Default is false. - */ - user_pin_required: preAuthorizedCodeFlowConfig.userPinRequired ?? false, - }, - } - } + const grants: Grant = { + 'urn:ietf:params:oauth:grant-type:pre-authorized_code': preAuthorizedCodeFlowConfig && { + 'pre-authorized_code': + preAuthorizedCodeFlowConfig.preAuthorizedCode ?? (await agentContext.wallet.generateNonce()), + user_pin_required: preAuthorizedCodeFlowConfig.userPinRequired ?? false, + }, - if (authorizationCodeFlowConfig) { - grants = { - ...grants, - authorization_code: { - issuer_state: authorizationCodeFlowConfig.issuerState ?? (await agentContext.wallet.generateNonce()), - }, - } + authorization_code: authorizationCodeFlowConfig && { + issuer_state: authorizationCodeFlowConfig.issuerState ?? (await agentContext.wallet.generateNonce()), + }, } return grants } - private mapInlineCredentialOfferIdToCredentialSupported(id: string, credentialsSupported: CredentialSupported[]) { - const credentialSupported = credentialsSupported.find((cs) => cs.id === id) - if (!credentialSupported) throw new AriesFrameworkError(`Credential supported with id '${id}' not found.`) - - return credentialSupported - } - - private getCredentialMetadata( - credential: CredentialSupported | CredentialOfferFormat - ): CredentialRequestJwtVc | CredentialRequestLdpVc { - if (credential.format === 'jwt_vc_json' || credential.format === 'jwt_vc_json-ld') { - return { - format: credential.format, - types: credential.types, - } - } else { - // TODO: - throw new AriesFrameworkError('Unsupported credential format') - } - } - - private getOfferedCredentialsMetadata( - credentials: (CredentialOfferFormat | string)[], - credentialsSupported: CredentialSupported[] - ) { - const credentialsReferencingCredentialsSupported = credentials - .filter((credential): credential is string => typeof credential === 'string') - .map((credentialId) => this.mapInlineCredentialOfferIdToCredentialSupported(credentialId, credentialsSupported)) - .map((credentialSupported) => this.getCredentialMetadata(credentialSupported)) - - const inlineCredentialOffers = credentials - .filter((credential): credential is CredentialOfferFormat => typeof credential !== 'string') - .map((credential) => this.getCredentialMetadata(credential)) - - return [...credentialsReferencingCredentialsSupported, ...inlineCredentialOffers] - } - public async createCredentialOfferAndRequest( agentContext: AgentContext, offeredCredentials: OfferedCredential[], @@ -338,7 +291,7 @@ export class OpenId4VcIssuerService { // this checks if the structure of the credentials is correct // it throws an error if a offered credential cannot be found in the credentialsSupported - this.getOfferedCredentialsMetadata(offeredCredentials, issuerMetadata.credentialsSupported) + getOfferedCredentialsWithMetadata(offeredCredentials, issuerMetadata.credentialsSupported) const vcIssuer = this.getVcIssuer(agentContext, issuerMetadata) @@ -348,7 +301,6 @@ export class OpenId4VcIssuerService { credentialOfferUri: options.credentialOfferUri, scheme: options.scheme ?? 'https', baseUri: options.baseUri ?? '', - // TODO: THIS IS WRONG HOW TO SPECIFY ldp_ creds? // credentialDefinition, }) @@ -393,26 +345,28 @@ export class OpenId4VcIssuerService { credentialOffer: CredentialOfferPayloadV1_0_11, credentialRequest: CredentialRequestV1_0_11, credentialsSupported: CredentialSupported[] - ) { - const offeredCredentials = this.getOfferedCredentialsMetadata(credentialOffer.credentials, credentialsSupported) + ): OfferedCredentialWithMetadata[] { + const offeredCredentials = getOfferedCredentialsWithMetadata(credentialOffer.credentials, credentialsSupported) return offeredCredentials.filter((offeredCredential) => { - if (credentialRequest.format === 'jwt_vc_json' && offeredCredential.format === 'jwt_vc_json') { - return equalsIgnoreOrder(offeredCredential.types, credentialRequest.types) - } else if (credentialRequest.format === 'jwt_vc_json-ld' && offeredCredential.format === 'jwt_vc_json-ld') { + if (offeredCredential.format !== credentialRequest.format) return false + + if (credentialRequest.format === OpenIdCredentialFormatProfile.JwtVcJson) { return equalsIgnoreOrder(offeredCredential.types, credentialRequest.types) - } else if (credentialRequest.format === 'ldp_vc' && offeredCredential.format === 'ldp_vc') { - return equalsIgnoreOrder( - offeredCredential.credential_definition.types, - credentialRequest.credential_definition.types - ) + } else if ( + credentialRequest.format === OpenIdCredentialFormatProfile.JwtVcJsonLd || + credentialRequest.format === OpenIdCredentialFormatProfile.LdpVc + ) { + return equalsIgnoreOrder(offeredCredential.types, credentialRequest.credential_definition.types) + } else if (credentialRequest.format === OpenIdCredentialFormatProfile.SdJwtVc) { + return equalsIgnoreOrder(offeredCredential.types, [credentialRequest.credential_definition.vct]) } }) } private getCredentialDataSupplier = ( agentContext: AgentContext, - credential: W3cCredential, + credential: string | W3cCredential, credentialsSupported: CredentialSupported[], issuerVerificationMethod: VerificationMethod ): CredentialDataSupplier => { @@ -429,24 +383,32 @@ export class OpenId4VcIssuerService { throw new AriesFrameworkError('No offered credential matches the requested credential.') } + if (credentialRequest.format === OpenIdCredentialFormatProfile.SdJwtVc) { + return { + format: credentialRequest.format, + credential: credential as any, // TODO: sdjwt + signCallback: this.getSdJwtVcCredentialSigningCallback(), + } + } + + if (typeof credential === 'string') { + throw new AriesFrameworkError( + `Credential must be a W3C credential if not using '${OpenIdCredentialFormatProfile.SdJwtVc}' format.` + ) + } + const issuedCredentialMatchesRequest = offeredCredentialsMatchingRequest.find( - (offeredCredential) => - ((offeredCredential.format === 'jwt_vc_json-ld' || offeredCredential.format === 'jwt_vc_json') && - equalsIgnoreOrder(offeredCredential.types, credential.type)) || - (offeredCredential.format === 'ldp_vc' && - equalsIgnoreOrder(offeredCredential.credential_definition.types, credential.type)) + (offeredCredential) => offeredCredential.types === credential.type ) if (!issuedCredentialMatchesRequest) { throw new AriesFrameworkError('The credential to be issued does not match the request.') } - const sphereonICredential = JsonTransformer.toJSON(credential) as ICredential - return { format: credentialRequest.format, - credential: sphereonICredential, - signCallback: this.getCredentialSigningCallback(agentContext, issuerVerificationMethod), + credential: JsonTransformer.toJSON(credential) as ICredential, + signCallback: this.getW3cCredentialSigningCallback(agentContext, issuerVerificationMethod), } } } @@ -494,7 +456,6 @@ export class OpenId4VcIssuerService { public configureRouter = (agentContext: AgentContext, router: Router, endpointConfig: EndpointConfig) => { // parse application/x-www-form-urlencoded router.use(bodyParser.urlencoded({ extended: false })) - // parse application/json router.use(bodyParser.json()) diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts index 473b60b1bb..08320b6760 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts @@ -1,23 +1,19 @@ -import type { SupportedCredentialFormats } from './issuance' import type { VerificationMethod, W3cCredential } from '@aries-framework/core' import type { - CommonCredentialSupported, + CredentialOfferFormat, CredentialOfferPayloadV1_0_11, CredentialRequestV1_0_11, + CredentialSupported, MetadataDisplay, ProofOfPossession, } from '@sphereon/oid4vci-common' -export type { MetadataDisplay, ProofOfPossession, CredentialOfferPayloadV1_0_11 } - -export interface CredentialOfferFormat { - format: SupportedCredentialFormats - types: string[] -} - -export interface CredentialSupported extends CommonCredentialSupported { - format: SupportedCredentialFormats - types: string[] +export type { + MetadataDisplay, + ProofOfPossession, + CredentialOfferPayloadV1_0_11, + CredentialSupported, + CredentialOfferFormat, } // If the entry is an object, the object contains the data related to a certain credential type @@ -67,7 +63,7 @@ export type CredentialOfferAndRequest = { export interface CreateIssueCredentialResponseOptions { credentialRequest: CredentialRequestV1_0_11 - credential: W3cCredential + credential: W3cCredential | string verificationMethod: VerificationMethod issuerMetadata?: IssuerMetadata } @@ -108,8 +104,9 @@ export interface AccessTokenEndpointConfig { export type CredentialRequestToCredentialMapper = ( credentialRequest: CredentialRequestV1_0_11, - holderDid: string -) => Promise + holderDid: string, + holderDidUrl: string +) => Promise export interface CredentialEndpointConfig { /** diff --git a/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderService.ts b/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderService.ts index 39fdaa2765..06933fd30f 100644 --- a/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderService.ts +++ b/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderService.ts @@ -1,14 +1,3 @@ -import type { - AuthCodeFlowOptions, - CredentialToRequest, - AcceptCredentialOfferOptions, - ProofOfPossessionRequirements, - ProofOfPossessionVerificationMethodResolver, - ResolvedCredentialOffer, - ResolvedAuthorizationRequest, - ResolvedAuthorizationRequestWithCode, - SupportedCredentialFormats, -} from './OpenId4VciHolderServiceOptions' import type { OfferedCredentialWithMetadata } from './utils/IssuerMetadataUtils' import type { AgentContext, @@ -24,15 +13,13 @@ import type { EndpointMetadataResult, Jwt, OpenIDResponse, - ProofOfPossessionCallbacks, PushedAuthorizationResponse, UniformCredentialOfferPayload, + AuthorizationDetails, } from '@sphereon/oid4vci-common' -import type { CredentialFormat } from '@sphereon/ssi-types' import { AriesFrameworkError, - ClaimFormat, Hasher, InjectionSymbols, JsonEncoder, @@ -55,6 +42,7 @@ import { equalsIgnoreOrder, getJwkClassFromKeyType, } from '@aries-framework/core' +import { SdJwtVcService, type SdJwtVcRecord } from '@aries-framework/sd-jwt-vc' import { AccessTokenClient, CredentialOfferClient, @@ -71,11 +59,23 @@ import { JsonURIMode, } from '@sphereon/oid4vci-common' -import { OpenIdCredentialFormatProfile, fromOpenIdCredentialFormatProfileToDifClaimFormat } from './utils' +import { + type AuthCodeFlowOptions, + type AcceptCredentialOfferOptions, + type ProofOfPossessionRequirements, + type ProofOfPossessionVerificationMethodResolver, + type ResolvedCredentialOffer, + type ResolvedAuthorizationRequest, + type ResolvedAuthorizationRequestWithCode, + type SupportedCredentialFormats, + supportedCredentialFormats, +} from './OpenId4VciHolderServiceOptions' +import { OpenIdCredentialFormatProfile } from './utils' import { getFormatForVersion, getUniformFormat } from './utils/Formats' import { getMetadataFromCredentialOffer, getOfferedCredentialsWithMetadata, + getSupportedCredentials, handleAuthorizationDetails, OfferedCredentialType, } from './utils/IssuerMetadataUtils' @@ -104,19 +104,7 @@ export function getSupportedJwaSignatureAlgorithms(agentContext: AgentContext): return supportedJwaSignatureAlgorithms } -export interface AuthDetails { - type: 'openid_credential' | string - locations?: string | string[] - format: CredentialFormat | CredentialFormat[] - - [s: string]: unknown -} - -function getV8CredentialType( - offeredCredentialWithMetadata: OfferedCredentialWithMetadata, - format: string, - version: OpenId4VCIVersion -) { +function getV8CredentialType(offeredCredentialWithMetadata: OfferedCredentialWithMetadata, version: OpenId4VCIVersion) { if (offeredCredentialWithMetadata.offerType === OfferedCredentialType.InlineCredentialOffer) { throw new AriesFrameworkError(`Inline credential offers not supported for version < 11`) } @@ -127,7 +115,7 @@ function getV8CredentialType( ) } - const originalFormat = getFormatForVersion(format, version) + const originalFormat = getFormatForVersion(offeredCredentialWithMetadata.format, version) const credentialType = offeredCredentialWithMetadata.credentialSupported.id.split(`-${originalFormat}`)[0] return credentialType } @@ -138,7 +126,7 @@ async function createAuthorizationRequestUri(options: { clientId: string codeChallenge: string codeChallengeMethod: CodeChallengeMethod - authDetails?: AuthDetails | AuthDetails[] + authDetails?: AuthorizationDetails | AuthorizationDetails[] redirectUri: string scope?: string[] }) { @@ -229,14 +217,8 @@ export class OpenId4VciHolderService { opts?: { version?: OpenId4VCIVersion } ): Promise { let version = opts?.version ?? OpenId4VCIVersion.VER_1_0_11 - const claimedCredentialOfferUrl = `openid-credential-offer://` - const claimedIssuanceInitiationUrl = `openid-initiate-issuance://` - if ( - typeof credentialOffer === 'string' && - (credentialOffer.startsWith(claimedCredentialOfferUrl) || - credentialOffer.startsWith(claimedIssuanceInitiationUrl)) - ) { + if (typeof credentialOffer === 'string' && URL.canParse(credentialOffer)) { const credentialOfferWithBaseUrl = await CredentialOfferClient.fromURI(credentialOffer) credentialOffer = credentialOfferWithBaseUrl.credential_offer version = credentialOfferWithBaseUrl.version @@ -258,77 +240,89 @@ export class OpenId4VciHolderService { this.logger.debug('Full server metadata', metadata) + const credentialsSupported = getSupportedCredentials({ issuerMetadata, version }) const offeredCredentialsWithMetadata = getOfferedCredentialsWithMetadata( - credentialOfferPayload, - issuerMetadata, - version + credentialOfferPayload.credentials, + credentialsSupported ) - const credentialsToRequest: CredentialToRequest[] = offeredCredentialsWithMetadata.map((offeredCredential) => { - const { format, types, offerType } = offeredCredential - if (offerType === OfferedCredentialType.InlineCredentialOffer) { - return { offerType, types, format } - } else { - const { id, cryptographic_binding_methods_supported, cryptographic_suites_supported } = - offeredCredential.credentialSupported - - return { id, offerType, cryptographic_binding_methods_supported, cryptographic_suites_supported, types, format } - } - }) - return { metadata, credentialOfferPayload, - credentialsToRequest, + offeredCredentials: offeredCredentialsWithMetadata, version, } } + private getScopeForOfferedCredential( + credentialWithMetadata: OfferedCredentialWithMetadata, + version: OpenId4VCIVersion + ): string | undefined { + const { format, offerType } = credentialWithMetadata + + // TODO: sdjwt + if (version <= OpenId4VCIVersion.VER_1_0_11) { + return undefined + } + + // TODO: sdjwt + if (offerType === OfferedCredentialType.CredentialSupported) { + const scope = + 'scope' in credentialWithMetadata.credentialSupported + ? credentialWithMetadata.credentialSupported.scope + : undefined + if (format === OpenIdCredentialFormatProfile.SdJwtVc && !scope) { + throw new AriesFrameworkError('Scope is required the request the issuance of a SdJwtVc') + } + + return scope as string + } + + return undefined + } + private getAuthDetailsFromOfferedCredential( credentialWithMetadata: OfferedCredentialWithMetadata, authDetailsLocation: string | undefined, version: OpenId4VCIVersion - ): AuthDetails | undefined { - const { format, types } = credentialWithMetadata + ): AuthorizationDetails | undefined { + const { format, types, offerType } = credentialWithMetadata const type = 'openid_credential' if (version < OpenId4VCIVersion.VER_1_0_11) { - const credential_type = getV8CredentialType(credentialWithMetadata, format, version) - return { type, credential_type, format } + // TODO: this is valid 08 + // const credentialType = getV8CredentialType(credentialWithMetadata, version) + // return { type, credential_type: credentialType, format } + return undefined } const locations = authDetailsLocation ? [authDetailsLocation] : undefined - if (format === OpenIdCredentialFormatProfile.JwtVcJson || format === OpenIdCredentialFormatProfile.JwtVcJsonLd) { - return { - type, - format, + if (format === OpenIdCredentialFormatProfile.JwtVcJson) { + return { type, format, types, locations } + } else if (format === OpenIdCredentialFormatProfile.LdpVc || format === OpenIdCredentialFormatProfile.JwtVcJsonLd) { + // Inline Credential Offers come with no context so we cannot create the authorization_details + // This type of credentials can only be requested via scopes + if (offerType === OfferedCredentialType.InlineCredentialOffer) return undefined + + const credential_definition = { + '@context': credentialWithMetadata.credentialSupported['@context'], types, - locations, + credentialSubject: credentialWithMetadata.credentialSupported.credentialSubject, } - } else if (format === OpenIdCredentialFormatProfile.LdpVc) { - let context: string | undefined = undefined - if (credentialWithMetadata.offerType === OfferedCredentialType.InlineCredentialOffer) { - // Inline Credential Offers come with no context so we cannot create the authorization_details - // This type of credentials can only be requested via scopes - return undefined - } else { - if ('@context' in credentialWithMetadata.credentialSupported) { - context = credentialWithMetadata.credentialSupported['@context'] as unknown as string - } else { - throw new AriesFrameworkError('Could not find @context in credentialSupported.') - } + return { type, format, locations, credential_definition } + } else if (format === OpenIdCredentialFormatProfile.SdJwtVc) { + const credential_definition = { + vct: types[0], + claims: + offerType === OfferedCredentialType.InlineCredentialOffer + ? credentialWithMetadata.credentialOffer.credential_definition.claims + : credentialWithMetadata.credentialSupported.credential_definition.claims, } - return { - type, - format, - types, - locations, - '@context': context, - } + return { type, format, locations, credential_definition } } else { - throw new AriesFrameworkError(`Cannot create authorization_details. Unsupported credential format ${format}.`) + throw new AriesFrameworkError(`Cannot create authorization_details. Unsupported credential format '${format}'.`) } } @@ -345,13 +339,19 @@ export class OpenId4VciHolderService { const codeChallenge = TypedArrayEncoder.toBase64URL(codeVerifierSha256) const { metadata, issuerMetadata } = await getMetadataFromCredentialOffer(credentialOfferPayload, _metadata) + const credentialsSupported = getSupportedCredentials({ issuerMetadata, version }) const offeredCredentialsWithMetadata = getOfferedCredentialsWithMetadata( - credentialOfferPayload, - issuerMetadata, - version + credentialOfferPayload.credentials, + credentialsSupported ) + this.logger.debug('Converted code_verifier to code_challenge', { + codeVerifier: codeVerifier, + sha256: codeVerifierSha256.toString(), + base64Url: codeChallenge, + }) + let authDetailsLocation: string | undefined if (issuerMetadata.authorization_server) { authDetailsLocation = metadata.issuer @@ -359,22 +359,22 @@ export class OpenId4VciHolderService { const authDetails = offeredCredentialsWithMetadata .map((credential) => this.getAuthDetailsFromOfferedCredential(credential, authDetailsLocation, version)) - .filter((authDetail): authDetail is AuthDetails => authDetail !== undefined) + .filter((authDetail): authDetail is AuthorizationDetails => authDetail !== undefined) - this.logger.debug('Converted code_verifier to code_challenge', { - codeVerifier: codeVerifier, - sha256: codeVerifierSha256.toString(), - base64Url: codeChallenge, - }) + const scopes = offeredCredentialsWithMetadata + .map((credential) => this.getScopeForOfferedCredential(credential, version)) + .filter((scope): scope is string => scope !== undefined) const { clientId, redirectUri, scope } = authCodeFlowOptions const authorizationRequestUri = await createAuthorizationRequestUri({ - credentialOffer: credentialOfferPayload, clientId, - codeChallengeMethod: CodeChallengeMethod.SHA256, codeChallenge, redirectUri, - scope, + credentialOffer: credentialOfferPayload, + codeChallengeMethod: CodeChallengeMethod.SHA256, + // TODO: sdjwt don't pass scope, it is always obtained from the metadata now + scope: [...(scope ?? []), ...scopes], + // TODO: should we now always use scopes instead of authDetails? or both???? authDetails, metadata, }) @@ -395,8 +395,8 @@ export class OpenId4VciHolderService { } ) { const { resolvedCredentialOffer, acceptCredentialOfferOptions, resolvedAuthorizationRequestWithCode } = options - const { credentialOfferPayload, metadata: _metadata, version } = resolvedCredentialOffer + const { credentialsToRequest, userPin, proofOfPossessionVerificationMethodResolver, verifyCredentialStatus } = acceptCredentialOfferOptions @@ -410,17 +410,17 @@ export class OpenId4VciHolderService { const { metadata, issuerMetadata } = await getMetadataFromCredentialOffer(credentialOfferPayload, _metadata) const supportedJwaSignatureAlgorithms = getSupportedJwaSignatureAlgorithms(agentContext) - const possibleProofOfPossessionSigAlgs = acceptCredentialOfferOptions.allowedProofOfPossessionSignatureAlgorithms - const allowedProofOfPossessionSignatureAlgorithms = possibleProofOfPossessionSigAlgs - ? possibleProofOfPossessionSigAlgs.filter((algorithm) => supportedJwaSignatureAlgorithms.includes(algorithm)) + const allowedProofOfPossessionSigAlgs = acceptCredentialOfferOptions.allowedProofOfPossessionSignatureAlgorithms + const possibleProofOfPossessionSigAlgs = allowedProofOfPossessionSigAlgs + ? allowedProofOfPossessionSigAlgs.filter((algorithm) => supportedJwaSignatureAlgorithms.includes(algorithm)) : supportedJwaSignatureAlgorithms - if (allowedProofOfPossessionSignatureAlgorithms.length === 0) { + if (possibleProofOfPossessionSigAlgs.length === 0) { throw new AriesFrameworkError( [ - `No supported proof of possession signature algorithm found.`, + `No possible proof of possession signature algorithm found.`, `Signature algorithms supported by the Agent '${supportedJwaSignatureAlgorithms.join(', ')}'`, - `Possible Signature algorithms '${possibleProofOfPossessionSigAlgs?.join(', ')}'`, + `Allowed Signature algorithms '${allowedProofOfPossessionSigAlgs?.join(', ')}'`, ].join('\n') ) } @@ -455,16 +455,16 @@ export class OpenId4VciHolderService { const accessToken = accessTokenResponse.successBody + const credentialsSupported = getSupportedCredentials({ issuerMetadata, version }) + const offeredCredentialsWithMetadata = getOfferedCredentialsWithMetadata( - credentialOfferPayload, - issuerMetadata, - version + credentialOfferPayload.credentials, + credentialsSupported ) const credentialsToRequestWithMetadata = credentialsToRequest?.map((ctr) => { const credentialToRequest = offeredCredentialsWithMetadata.find((offeredCredentialWithMetadata) => { const { format, types } = offeredCredentialWithMetadata - // only requests credentials with the exact same set of types and format return ctr.format === format && equalsIgnoreOrder(ctr.types, types) }) @@ -479,28 +479,22 @@ export class OpenId4VciHolderService { return credentialToRequest }) - const receivedCredentials: W3cCredentialRecord[] = [] + const receivedCredentials: (W3cCredentialRecord | SdJwtVcRecord)[] = [] let newCNonce: string | undefined - // Loop through all the credentialTypes in the credential offer for (const credentialWithMetadata of credentialsToRequestWithMetadata ?? offeredCredentialsWithMetadata) { // Get all options for the credential request (such as which kid to use, the signature algorithm, etc) const { verificationMethod, signatureAlgorithm } = await this.getCredentialRequestOptions(agentContext, { - allowedCredentialFormats: [OpenIdCredentialFormatProfile.JwtVcJson, OpenIdCredentialFormatProfile.JwtVcJsonLd], - allowedProofOfPossessionSignatureAlgorithms, + possibleProofOfPossessionSignatureAlgorithms: possibleProofOfPossessionSigAlgs, offeredCredentialWithMetadata: credentialWithMetadata, proofOfPossessionVerificationMethodResolver, }) - const callbacks: ProofOfPossessionCallbacks = { - signCallback: this.signCallback(agentContext, verificationMethod), - } - // Create the proof of possession const proofOfPossessionBuilder = ProofOfPossessionBuilder.fromAccessTokenResponse({ accessTokenResponse: accessToken, - callbacks, + callbacks: { signCallback: this.signCallback(agentContext, verificationMethod) }, version, }) .withEndpointMetadata(metadata) @@ -520,36 +514,35 @@ export class OpenId4VciHolderService { .withCredentialEndpoint(metadata.credential_endpoint) .withTokenFromResponse(accessToken) - const isInlineOffer = credentialWithMetadata.offerType === OfferedCredentialType.InlineCredentialOffer const format = credentialWithMetadata.format - const originalFormat = getFormatForVersion(format, version) let credentialTypes: string | string[] if (version < OpenId4VCIVersion.VER_1_0_11) { - if (isInlineOffer) throw new AriesFrameworkError(`Inline credential offers not supported for version < 11`) - credentialTypes = getV8CredentialType(credentialWithMetadata, format, version) + if (credentialWithMetadata.offerType === OfferedCredentialType.InlineCredentialOffer) { + throw new AriesFrameworkError(`Inline credential offers not supported for version < 11`) + } + credentialTypes = getV8CredentialType(credentialWithMetadata, version) } else { - if (isInlineOffer) credentialTypes = credentialWithMetadata.inlineCredentialOffer.types - else credentialTypes = credentialWithMetadata.credentialSupported.types + credentialTypes = credentialWithMetadata.types } const credentialRequestClient = credentialRequestBuilder.build() const credentialResponse = await credentialRequestClient.acquireCredentialsUsingProof({ proofInput: proofOfPossession, credentialTypes, - format: originalFormat, + format: getFormatForVersion(format, version), }) newCNonce = credentialResponse.successBody?.c_nonce - const credential = await this.handleCredentialResponse(agentContext, credentialResponse, { + // Create credential record, but we don't store it yet (only after the user has accepted the credential) + const credentialRecord = await this.handleCredentialResponse(agentContext, credentialResponse, { verifyCredentialStatus: verifyCredentialStatus ?? false, + holderDidUrl: verificationMethod.id, + issuerDidUrl: verificationMethod.controller, // TODO: how to figure this out? }) - // Create credential record, but we don't store it yet (only after the user has accepted the credential) - const credentialRecord = new W3cCredentialRecord({ credential, tags: { expandedTypes: [] } }) this.logger.debug('Full credential', credentialRecord) - receivedCredentials.push(credentialRecord) } @@ -566,17 +559,15 @@ export class OpenId4VciHolderService { agentContext: AgentContext, options: { proofOfPossessionVerificationMethodResolver: ProofOfPossessionVerificationMethodResolver - allowedCredentialFormats: SupportedCredentialFormats[] - allowedProofOfPossessionSignatureAlgorithms: JwaSignatureAlgorithm[] + possibleProofOfPossessionSignatureAlgorithms: JwaSignatureAlgorithm[] offeredCredentialWithMetadata: OfferedCredentialWithMetadata } ) { const { signatureAlgorithm, supportedDidMethods, supportsAllDidMethods } = this.getProofOfPossessionRequirements( agentContext, { - offeredCredentialWithMetadata: options.offeredCredentialWithMetadata, - allowedCredentialFormats: options.allowedCredentialFormats, - allowedProofOfPossessionSignatureAlgorithms: options.allowedProofOfPossessionSignatureAlgorithms, + credentialsToRequest: options.offeredCredentialWithMetadata, + possibleProofOfPossessionSignatureAlgorithms: options.possibleProofOfPossessionSignatureAlgorithms, } ) @@ -641,29 +632,20 @@ export class OpenId4VciHolderService { private getProofOfPossessionRequirements( agentContext: AgentContext, options: { - offeredCredentialWithMetadata: OfferedCredentialWithMetadata - allowedCredentialFormats: SupportedCredentialFormats[] - allowedProofOfPossessionSignatureAlgorithms: JwaSignatureAlgorithm[] + credentialsToRequest: OfferedCredentialWithMetadata + possibleProofOfPossessionSignatureAlgorithms: JwaSignatureAlgorithm[] } ): ProofOfPossessionRequirements { - const { offeredCredentialWithMetadata, allowedCredentialFormats } = options - const isInlineOffer = offeredCredentialWithMetadata.offerType === OfferedCredentialType.InlineCredentialOffer - const format = offeredCredentialWithMetadata.format - - const credentialSupportedMetadata = isInlineOffer ? undefined : offeredCredentialWithMetadata.credentialSupported - - const issuerSupportedCryptographicSuites = credentialSupportedMetadata?.cryptographic_suites_supported - const issuerSupportedBindingMethods = credentialSupportedMetadata?.cryptographic_binding_methods_supported + const { credentialsToRequest } = options - if (!isInlineOffer) { - const credentialMetadata = offeredCredentialWithMetadata.credentialSupported - if (!allowedCredentialFormats.includes(format as SupportedCredentialFormats)) { + if (credentialsToRequest.offerType === OfferedCredentialType.CredentialSupported) { + if (!supportedCredentialFormats.includes(credentialsToRequest.format as SupportedCredentialFormats)) { throw new AriesFrameworkError( [ - `The issuer only supports format '${format}'`, - `for the credential type '${credentialMetadata.types.join(', ')}`, - `but the wallet only allows formats '${options.allowedCredentialFormats.join(', ')}'`, - ].join(' ') + `Requested credential with format '${credentialsToRequest.format}',`, + `for the credential of type '${credentialsToRequest.types.join(', ')},`, + `but the wallet only supports the following formats '${supportedCredentialFormats.join(', ')}'`, + ].join('\n') ) } } @@ -673,21 +655,29 @@ export class OpenId4VciHolderService { let signatureAlgorithm: JwaSignatureAlgorithm | undefined + const credentialSupported = + credentialsToRequest.offerType === OfferedCredentialType.CredentialSupported + ? credentialsToRequest.credentialSupported + : undefined + + const issuerSupportedCryptographicSuites = credentialSupported?.cryptographic_suites_supported + const issuerSupportedBindingMethods = credentialSupported?.cryptographic_binding_methods_supported + // If undefined, it means the issuer didn't include the cryptographic suites in the metadata // We just guess that the first one is supported if (issuerSupportedCryptographicSuites === undefined) { - signatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms[0] + signatureAlgorithm = options.possibleProofOfPossessionSignatureAlgorithms[0] } else { - switch (format) { - case 'jwt_vc_json': - case 'jwt_vc_json-ld': - signatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms.find((signatureAlgorithm) => + switch (credentialsToRequest.format) { + case OpenIdCredentialFormatProfile.JwtVcJson: + case OpenIdCredentialFormatProfile.JwtVcJsonLd: + case OpenIdCredentialFormatProfile.SdJwtVc: + signatureAlgorithm = options.possibleProofOfPossessionSignatureAlgorithms.find((signatureAlgorithm) => issuerSupportedCryptographicSuites.includes(signatureAlgorithm) ) break - case 'ldp_vc': - // We need to find it based on the JSON-LD proof type - signatureAlgorithm = options.allowedProofOfPossessionSignatureAlgorithms.find((signatureAlgorithm) => { + case OpenIdCredentialFormatProfile.LdpVc: + signatureAlgorithm = options.possibleProofOfPossessionSignatureAlgorithms.find((signatureAlgorithm) => { const JwkClass = getJwkClassFromJwaSignatureAlgorithm(signatureAlgorithm) if (!JwkClass) return false @@ -698,14 +688,14 @@ export class OpenId4VciHolderService { }) break default: - throw new AriesFrameworkError(`Unsupported credential format. Requested format '${format}'`) + throw new AriesFrameworkError(`Unsupported credential format.`) } } if (!signatureAlgorithm) { throw new AriesFrameworkError( - `Could not establish signature algorithm for format ${format} and id ${ - credentialSupportedMetadata?.id ?? 'Inline credential offer' + `Could not establish signature algorithm for format ${credentialsToRequest.format} and id ${ + credentialSupported?.id ?? 'Inline credential offer' }` ) } @@ -722,26 +712,46 @@ export class OpenId4VciHolderService { private async handleCredentialResponse( agentContext: AgentContext, credentialResponse: OpenIDResponse, - options: { verifyCredentialStatus: boolean } - ) { - const { verifyCredentialStatus } = options + options: { verifyCredentialStatus: boolean; holderDidUrl: string; issuerDidUrl: string } + ): Promise { + const { verifyCredentialStatus, holderDidUrl } = options this.logger.debug('Credential request response', credentialResponse) if (!credentialResponse.successBody) { throw new AriesFrameworkError('Did not receive a successful credential response.') } - const format = getUniformFormat(credentialResponse.successBody.format) as OpenIdCredentialFormatProfile - const difClaimFormat = fromOpenIdCredentialFormatProfileToDifClaimFormat(format) + const format = getUniformFormat(credentialResponse.successBody.format) + + if (format === OpenIdCredentialFormatProfile.SdJwtVc) { + const sdJwtVcService = agentContext.dependencyManager.resolve(SdJwtVcService) + if (!sdJwtVcService) + throw new AriesFrameworkError('Received an SdJwtVc but no SdJwtVc-Module available for the agent.') + + if (typeof credentialResponse.successBody.credential !== 'string') + throw new AriesFrameworkError( + `Received a credential of format ${ + OpenIdCredentialFormatProfile.SdJwtVc + }, but the credential is not a string. ${JSON.stringify(credentialResponse.successBody.credential)}` + ) + + // TODO + const sdJwtVcRecord = await sdJwtVcService.fromString(agentContext, credentialResponse.successBody.credential, { + holderDidUrl, + issuerDidUrl: + 'did:key:z6MktiQQEqm2yapXBDt1WEVB3dqgvyzi96FuFANYmrgTrKV9#z6MktiQQEqm2yapXBDt1WEVB3dqgvyzi96FuFANYmrgTrKV9', + }) + + return sdJwtVcRecord + } let credential: W3cVerifiableCredential let result: W3cVerifyCredentialResult - - if (difClaimFormat === ClaimFormat.LdpVc) { + if (format === OpenIdCredentialFormatProfile.JwtVcJson || format === OpenIdCredentialFormatProfile.JwtVcJsonLd) { // validate json-ld credentials credential = JsonTransformer.fromJSON(credentialResponse.successBody.credential, W3cJsonLdVerifiableCredential) result = await this.w3cCredentialService.verifyCredential(agentContext, { credential, verifyCredentialStatus }) - } else if (difClaimFormat === ClaimFormat.JwtVc) { + } else if (format === OpenIdCredentialFormatProfile.LdpVc) { // validate jwt credentials credential = W3cJwtVerifiableCredential.fromSerializedJwt(credentialResponse.successBody.credential as string) result = await this.w3cCredentialService.verifyCredential(agentContext, { credential, verifyCredentialStatus }) @@ -754,7 +764,7 @@ export class OpenId4VciHolderService { throw new AriesFrameworkError(`Failed to validate credential, error = ${result.error?.message ?? 'Unknown'}`) } - return credential + return new W3cCredentialRecord({ credential, tags: { expandedTypes: [] } }) } private signCallback(agentContext: AgentContext, verificationMethod: VerificationMethod) { @@ -777,8 +787,6 @@ export class OpenId4VciHolderService { ) } - const payload = JsonEncoder.toBuffer(jwt.payload) - // We don't support these properties, remove them, so we can pass all other header properties to the JWS service if (jwt.header.x5c || jwt.header.jwk) throw new AriesFrameworkError('x5c and jwk are not supported') @@ -787,7 +795,7 @@ export class OpenId4VciHolderService { const jws = await this.jwsService.createJwsCompact(agentContext, { key, - payload, + payload: JsonEncoder.toBuffer(jwt.payload), protectedHeaderOptions: supportedHeaderOptions, }) diff --git a/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderServiceOptions.ts b/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderServiceOptions.ts index 7212df7642..80aeb69de5 100644 --- a/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderServiceOptions.ts +++ b/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderServiceOptions.ts @@ -1,27 +1,32 @@ -import type { OfferedCredentialType } from './utils/IssuerMetadataUtils' -import type { OpenIdCredentialFormatProfile } from './utils/claimFormatMapping' +import type { OfferedCredentialWithMetadata } from './utils/IssuerMetadataUtils' import type { JwaSignatureAlgorithm, KeyType, VerificationMethod } from '@aries-framework/core' -import type { CredentialOfferPayloadV1_0_11, EndpointMetadataResult, OpenId4VCIVersion } from '@sphereon/oid4vci-common' +import type { + CredentialOfferPayloadV1_0_11, + EndpointMetadataResult, + OpenId4VCIVersion, + AuthorizationDetails, +} from '@sphereon/oid4vci-common' -export type SupportedCredentialFormats = 'jwt_vc_json' | 'jwt_vc_json-ld' +import { OpenIdCredentialFormatProfile } from './utils/claimFormatMapping' -export type { OfferedCredentialType, OpenId4VCIVersion, EndpointMetadataResult, CredentialOfferPayloadV1_0_11 } +export type SupportedCredentialFormats = + | OpenIdCredentialFormatProfile.JwtVcJson + | OpenIdCredentialFormatProfile.JwtVcJsonLd + | OpenIdCredentialFormatProfile.SdJwtVc -export type CredentialToRequest = { format: OpenIdCredentialFormatProfile; types: string[] } & ( - | { offerType: OfferedCredentialType.InlineCredentialOffer } - | { - offerType: OfferedCredentialType.CredentialSupported - id: string | undefined - cryptographic_binding_methods_supported: string[] | undefined - cryptographic_suites_supported: string[] | undefined - } -) +export const supportedCredentialFormats: SupportedCredentialFormats[] = [ + OpenIdCredentialFormatProfile.JwtVcJson, + OpenIdCredentialFormatProfile.JwtVcJsonLd, + OpenIdCredentialFormatProfile.SdJwtVc, +] + +export type { OpenId4VCIVersion, EndpointMetadataResult, CredentialOfferPayloadV1_0_11, AuthorizationDetails } export interface ResolvedCredentialOffer { metadata: EndpointMetadataResult credentialOfferPayload: CredentialOfferPayloadV1_0_11 version: OpenId4VCIVersion - credentialsToRequest: CredentialToRequest[] + offeredCredentials: OfferedCredentialWithMetadata[] } export interface ResolvedAuthorizationRequest extends AuthCodeFlowOptions { @@ -47,7 +52,7 @@ export interface AcceptCredentialOfferOptions { * This is the list of credentials that will be requested from the issuer. * If not provided all offered credentials will be requested. */ - credentialsToRequest?: CredentialToRequest[] + credentialsToRequest?: OfferedCredentialWithMetadata[] verifyCredentialStatus?: boolean diff --git a/packages/openid4vc-issuer/src/issuance/utils/IssuerMetadataUtils.ts b/packages/openid4vc-issuer/src/issuance/utils/IssuerMetadataUtils.ts index 63aec9bd60..1441160066 100644 --- a/packages/openid4vc-issuer/src/issuance/utils/IssuerMetadataUtils.ts +++ b/packages/openid4vc-issuer/src/issuance/utils/IssuerMetadataUtils.ts @@ -1,10 +1,17 @@ -import type { OpenIdCredentialFormatProfile } from './claimFormatMapping' -import type { AuthDetails } from '../OpenId4VciHolderService' import type { + AuthorizationDetails, + CommonCredentialOfferFormat, + CommonCredentialSupported, CredentialIssuerMetadata, CredentialOfferFormat, + CredentialOfferFormatJwtVcJson, + CredentialOfferFormatJwtVcJsonLdAndLdpVc, + CredentialOfferFormatSdJwtVc, CredentialOfferPayloadV1_0_11, CredentialSupported, + CredentialSupportedJwtVcJson, + CredentialSupportedJwtVcJsonLdAndLdpVc, + CredentialSupportedSdJwtVc, CredentialSupportedTypeV1_0_08, CredentialSupportedV1_0_08, EndpointMetadataResult, @@ -16,6 +23,7 @@ import { MetadataClient } from '@sphereon/oid4vci-client' import { OpenId4VCIVersion } from '@sphereon/oid4vci-common' import { getUniformFormat } from './Formats' +import { OpenIdCredentialFormatProfile } from './claimFormatMapping' /** * The type of a credential offer entry. For each item in `credentials` array, the type MUST be one of the following: @@ -29,15 +37,39 @@ export enum OfferedCredentialType { export type OfferedCredentialWithMetadata = | { - credentialSupported: CredentialSupported offerType: OfferedCredentialType.CredentialSupported - format: OpenIdCredentialFormatProfile + format: OpenIdCredentialFormatProfile.JwtVcJson + credentialSupported: CommonCredentialSupported & CredentialSupportedJwtVcJson + types: string[] + } + | { + offerType: OfferedCredentialType.CredentialSupported + format: OpenIdCredentialFormatProfile.JwtVcJsonLd | OpenIdCredentialFormatProfile.LdpVc + credentialSupported: CommonCredentialSupported & CredentialSupportedJwtVcJsonLdAndLdpVc + types: string[] + } + | { + offerType: OfferedCredentialType.CredentialSupported + format: OpenIdCredentialFormatProfile.SdJwtVc + credentialSupported: CommonCredentialSupported & CredentialSupportedSdJwtVc + types: string[] + } + | { + offerType: OfferedCredentialType.InlineCredentialOffer + format: OpenIdCredentialFormatProfile.JwtVcJson + credentialOffer: CommonCredentialOfferFormat & CredentialOfferFormatJwtVcJson types: string[] } | { - inlineCredentialOffer: CredentialOfferFormat offerType: OfferedCredentialType.InlineCredentialOffer - format: OpenIdCredentialFormatProfile + format: OpenIdCredentialFormatProfile.JwtVcJsonLd | OpenIdCredentialFormatProfile.LdpVc + credentialOffer: CommonCredentialOfferFormat & CredentialOfferFormatJwtVcJsonLdAndLdpVc + types: string[] + } + | { + offerType: OfferedCredentialType.InlineCredentialOffer + format: OpenIdCredentialFormatProfile.SdJwtVc + credentialOffer: CommonCredentialOfferFormat & CredentialOfferFormatSdJwtVc types: string[] } @@ -50,15 +82,12 @@ export type OfferedCredentialWithMetadata = * id will be the `-`. */ export function getOfferedCredentialsWithMetadata( - credentialOfferPayload: CredentialOfferPayloadV1_0_11, - issuerMetadata: CredentialIssuerMetadata | IssuerMetadataV1_0_08, - version: OpenId4VCIVersion + credentialOffers: (CredentialOfferFormat | string)[], + supportedCredentials: CredentialSupported[] ) { - const offeredCredentials: OfferedCredentialWithMetadata[] = [] + const offeredCredentialsWithMetadata: OfferedCredentialWithMetadata[] = [] - const supportedCredentials = getSupportedCredentials({ issuerMetadata, version }) - - for (const offeredCredential of credentialOfferPayload.credentials) { + for (const offeredCredential of credentialOffers) { // If the offeredCredential is a string, it has to reference a supported credential in the issuer metadata if (typeof offeredCredential === 'string') { const foundSupportedCredentials = supportedCredentials.filter( @@ -75,26 +104,46 @@ export function getOfferedCredentialsWithMetadata( } for (const foundSupportedCredential of foundSupportedCredentials) { - offeredCredentials.push({ - credentialSupported: foundSupportedCredential, - offerType: OfferedCredentialType.CredentialSupported, - format: getUniformFormat(foundSupportedCredential.format), - types: foundSupportedCredential.types, - }) + if (foundSupportedCredential.format === 'vc+sd-jwt') { + offeredCredentialsWithMetadata.push({ + offerType: OfferedCredentialType.CredentialSupported, + credentialSupported: foundSupportedCredential, + format: OpenIdCredentialFormatProfile.SdJwtVc, + types: [foundSupportedCredential.credential_definition.vct], + }) + } else { + offeredCredentialsWithMetadata.push({ + offerType: OfferedCredentialType.CredentialSupported, + credentialSupported: foundSupportedCredential, + format: getUniformFormat(foundSupportedCredential.format), + types: foundSupportedCredential.types, + } as OfferedCredentialWithMetadata) + } } } // Otherwise it's an inline credential offer that does not reference a supported credential in the issuer metadata else { - offeredCredentials.push({ - inlineCredentialOffer: offeredCredential, + let types: string[] + if (offeredCredential.format === 'jwt_vc_json') { + types = offeredCredential.types + } else if (offeredCredential.format === 'jwt_vc_json-ld' || offeredCredential.format === 'ldp_vc') { + types = offeredCredential.credential_definition.types + } else if (offeredCredential.format === 'vc+sd-jwt') { + types = [offeredCredential.credential_definition.vct] + } else { + throw new AriesFrameworkError(`Unknown format received ${JSON.stringify(offeredCredential.format)}`) + } + + offeredCredentialsWithMetadata.push({ offerType: OfferedCredentialType.InlineCredentialOffer, format: getUniformFormat(offeredCredential.format), - types: offeredCredential.types, - }) + types: types, + credentialOffer: offeredCredential, + } as OfferedCredentialWithMetadata) } } - return offeredCredentials + return offeredCredentialsWithMetadata } export async function getMetadataFromCredentialOffer( @@ -103,8 +152,9 @@ export async function getMetadataFromCredentialOffer( ) { const issuer = credentialOfferPayload.credential_issuer - const resolvedMetadata = - metadata && metadata.credentialIssuerMetadata ? metadata : await MetadataClient.retrieveAllMetadata(issuer) + const resolvedMetadata = metadata?.credentialIssuerMetadata + ? metadata + : await MetadataClient.retrieveAllMetadata(issuer) if (!resolvedMetadata) { throw new AriesFrameworkError(`Could not retrieve metadata for OpenId4Vci issuer: ${issuer}`) @@ -143,21 +193,21 @@ export function getSupportedCredentials(opts: { export function credentialsSupportedV8ToV11(supportedV8: CredentialSupportedTypeV1_0_08): CredentialSupported[] { return Object.entries(supportedV8).flatMap((entry) => { - const type = entry[0] + const credentialId = entry[0] const supportedV8 = entry[1] - return credentialSupportedV8ToV11(type, supportedV8) + return credentialSupportedV8ToV11(credentialId, supportedV8) }) } export function credentialSupportedV8ToV11( - key: string, + credentialId: string, supportedV8: CredentialSupportedV1_0_08 ): CredentialSupported[] { const v8FormatEntries = Object.entries(supportedV8.formats) return v8FormatEntries.map((entry) => { const format = entry[0] - const credentialSupportBrief = entry[1] + const credentialSupportedV8 = entry[1] if (typeof format !== 'string') { throw Error(`Unknown format received ${JSON.stringify(format)}`) } @@ -166,25 +216,28 @@ export function credentialSupportedV8ToV11( // v11 format has an array where each entry only supports one format, and can only have an `id` property. We include the // key from the v8 object as the id for the v11 object, but to prevent collisions (as multiple formats can be supported under // one key), we append the format to the key IF there's more than one format supported under the key. - const id = v8FormatEntries.length > 1 ? `${key}-${format}` : key + const id = v8FormatEntries.length > 1 ? `${credentialId}-${format}` : credentialId let credentialSupported: CredentialSupported - if (format === 'jwt_vc_json') { + const v11Format = getUniformFormat(format) + if (v11Format === OpenIdCredentialFormatProfile.JwtVcJson) { credentialSupported = { - format, + format: OpenIdCredentialFormatProfile.JwtVcJson, display: supportedV8.display, - ...credentialSupportBrief, + ...credentialSupportedV8, credentialSubject: supportedV8.claims, id, } - } else { + } else if (v11Format === OpenIdCredentialFormatProfile.JwtVcJsonLd) { credentialSupported = { - format, + format: v11Format, display: supportedV8.display, - ...credentialSupportBrief, + ...credentialSupportedV8, id, '@context': ['VerifiableCredential'], // NOTE: V8 credentials don't come with @context } + } else { + throw new AriesFrameworkError(`Invalid format received for OpenId4Vci V8 '${format}'`) } return credentialSupported @@ -193,20 +246,21 @@ export function credentialSupportedV8ToV11( // copied from sphereon export function handleAuthorizationDetails( - authorizationDetails: AuthDetails | AuthDetails[], + authorizationDetails: AuthorizationDetails | AuthorizationDetails[], metadata: EndpointMetadataResult -): AuthDetails | AuthDetails[] | undefined { +): AuthorizationDetails | AuthorizationDetails[] | undefined { if (Array.isArray(authorizationDetails)) { - return authorizationDetails.map((value) => handleLocations({ ...value }, metadata)) + return authorizationDetails.map((value) => handleLocations(value, metadata)) } else { - return handleLocations({ ...authorizationDetails }, metadata) + return handleLocations(authorizationDetails, metadata) } } // copied from sphereon -export function handleLocations(authorizationDetails: AuthDetails, metadata: EndpointMetadataResult) { +export function handleLocations(authorizationDetails: AuthorizationDetails, metadata: EndpointMetadataResult) { + if (typeof authorizationDetails === 'string') return authorizationDetails if (metadata.credentialIssuerMetadata?.authorization_server || metadata.authorization_endpoint) { - if (!authorizationDetails.locations) authorizationDetails.locations = metadata.issuer + if (!authorizationDetails.locations) authorizationDetails.locations = [metadata.issuer] else if (Array.isArray(authorizationDetails.locations)) authorizationDetails.locations.push(metadata.issuer) else authorizationDetails.locations = [authorizationDetails.locations as string, metadata.issuer] } diff --git a/packages/openid4vc-issuer/src/issuance/utils/__tests__/claimFormatMapping.test.ts b/packages/openid4vc-issuer/src/issuance/utils/__tests__/claimFormatMapping.test.ts index a8bcdd9633..193c4cb544 100644 --- a/packages/openid4vc-issuer/src/issuance/utils/__tests__/claimFormatMapping.test.ts +++ b/packages/openid4vc-issuer/src/issuance/utils/__tests__/claimFormatMapping.test.ts @@ -37,9 +37,5 @@ describe('claimFormatMapping', () => { expect(fromOpenIdCredentialFormatProfileToDifClaimFormat(OpenIdCredentialFormatProfile.LdpVc)).toStrictEqual( ClaimFormat.LdpVc ) - - expect(() => fromOpenIdCredentialFormatProfileToDifClaimFormat(OpenIdCredentialFormatProfile.MsoMdoc)).toThrow( - AriesFrameworkError - ) }) }) diff --git a/packages/openid4vc-issuer/src/issuance/utils/claimFormatMapping.ts b/packages/openid4vc-issuer/src/issuance/utils/claimFormatMapping.ts index 3ab952f94f..26a59e46fc 100644 --- a/packages/openid4vc-issuer/src/issuance/utils/claimFormatMapping.ts +++ b/packages/openid4vc-issuer/src/issuance/utils/claimFormatMapping.ts @@ -4,7 +4,7 @@ export enum OpenIdCredentialFormatProfile { JwtVcJson = 'jwt_vc_json', JwtVcJsonLd = 'jwt_vc_json-ld', LdpVc = 'ldp_vc', - MsoMdoc = 'mso_mdoc', + SdJwtVc = 'vc+sd-jwt', } export const fromDifClaimFormatToOpenIdCredentialFormatProfile = ( diff --git a/packages/openid4vc-issuer/src/issuance/utils/index.ts b/packages/openid4vc-issuer/src/issuance/utils/index.ts index 761d341d06..5bee9d1118 100644 --- a/packages/openid4vc-issuer/src/issuance/utils/index.ts +++ b/packages/openid4vc-issuer/src/issuance/utils/index.ts @@ -1,2 +1,3 @@ export * from './claimFormatMapping' export * from './Formats' +export { OfferedCredentialType, OfferedCredentialWithMetadata } from './IssuerMetadataUtils' diff --git a/packages/openid4vc-issuer/src/router/OpenId4VcIEndpointConfiguration.ts b/packages/openid4vc-issuer/src/router/OpenId4VcIEndpointConfiguration.ts index c918733ab7..d7472d49bf 100644 --- a/packages/openid4vc-issuer/src/router/OpenId4VcIEndpointConfiguration.ts +++ b/packages/openid4vc-issuer/src/router/OpenId4VcIEndpointConfiguration.ts @@ -4,6 +4,7 @@ import type { MetadataEndpointConfig, CredentialEndpointConfig, AccessTokenEndpointConfig, + CredentialSupported, } from '../OpenId4VcIssuerServiceOptions' import type { CNonceState, @@ -11,7 +12,6 @@ import type { CredentialOfferSession, CredentialRequestV1_0_11, CredentialResponse, - CredentialSupported, IStateManager, } from '@sphereon/oid4vci-common' import type { Router, Request, Response } from 'express' @@ -117,7 +117,7 @@ export function configureCredentialEndpoint( const didDocument = await didsApi.resolveDidDocument(kid) const holderDid = didDocument.id - const credential = await credentialRequestToCredentialMapper(credentialRequest, holderDid) + const credential = await credentialRequestToCredentialMapper(credentialRequest, holderDid, kid) const issueCredentialResponse = await config.createIssueCredentialResponse(agentContext, { credentialRequest, diff --git a/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts b/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts index d14db6af88..ebddf5120f 100644 --- a/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts +++ b/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts @@ -39,25 +39,34 @@ import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { cleanAll, enableNetConnect } from 'nock' import { equalsIgnoreOrder } from '../../core/src/utils/deepEquality' -import { OpenId4VcIssuerModule, OpenId4VcIssuerService } from '../src' +import { SdJwtVcModule } from '../../sd-jwt-vc/src/SdJwtVcModule' +import { OpenIdCredentialFormatProfile, OpenId4VcIssuerModule, OpenId4VcIssuerService } from '../src' -const openBadgeCredential: CredentialSupported & { id: string } = { +const openBadgeCredential = { id: 'https://openid4vc-issuer.com/credentials/OpenBadgeCredential', - format: 'jwt_vc_json', + format: OpenIdCredentialFormatProfile.JwtVcJson, types: ['VerifiableCredential', 'OpenBadgeCredential'], -} +} satisfies CredentialSupported & { id: string } -const universityDegreeCredential: CredentialSupported & { id: string } = { +const universityDegreeCredential = { id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredential', - format: 'jwt_vc_json', + format: OpenIdCredentialFormatProfile.JwtVcJson, types: ['VerifiableCredential', 'UniversityDegreeCredential'], -} +} satisfies CredentialSupported & { id: string } -const universityDegreeCredentialLd: CredentialSupported & { id: string } = { +const universityDegreeCredentialLd = { id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredentialLd', - format: 'jwt_vc_json-ld', + format: OpenIdCredentialFormatProfile.JwtVcJson, types: ['VerifiableCredential', 'UniversityDegreeCredential'], -} +} satisfies CredentialSupported & { id: string } + +const universityDegreeCredentialSdJwt = { + id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredentialSdJwt', + format: OpenIdCredentialFormatProfile.SdJwtVc, + credential_definition: { + vct: 'UniversityDegreeCredential', + }, +} satisfies CredentialSupported & { id: string } const baseCredentialRequestOptions = { scheme: 'openid-credential-offer', @@ -68,11 +77,12 @@ const issuerMetadata: IssuerMetadata = { credentialIssuer: 'https://openid4vc-issuer.com', credentialEndpoint: 'https://openid4vc-issuer.com/credentials', tokenEndpoint: 'https://openid4vc-issuer.com/token', - credentialsSupported: [openBadgeCredential, universityDegreeCredentialLd], + credentialsSupported: [openBadgeCredential, universityDegreeCredentialLd, universityDegreeCredentialSdJwt], } const modules = { openId4VcIssuer: new OpenId4VcIssuerModule({ issuerMetadata }), + sdJwtVc: new SdJwtVcModule(), askar: new AskarModule({ ariesAskar }), } @@ -82,7 +92,7 @@ const createCredentialRequestFromKid = async ( agentContext: AgentContext, options: { issuerMetadata: IssuerMetadata - format: 'jwt_vc_json' | 'jwt_vc_json-ld' + format: OpenIdCredentialFormatProfile types: string[] nonce: string kid: string @@ -120,46 +130,23 @@ const createCredentialRequestFromKid = async ( key, }) - return { - format, - types, - proof: { jwt: jws, proof_type: 'jwt' }, - } -} - -async function handleCredentialResponse( - agentContext: AgentContext, - sphereonVerifiableCredential: SphereonW3cVerifiableCredential, - format: string, - types: string[] -) { - const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) - - let result: W3cVerifyCredentialResult - let w3cVerifiableCredential: W3cVerifiableCredential - - if (typeof sphereonVerifiableCredential === 'string') { - if (format !== 'jwt_vc_json' && format !== 'jwt_vc_json-ld') throw new Error('Invalid format') - // validate json-ld credentials - w3cVerifiableCredential = W3cJwtVerifiableCredential.fromSerializedJwt(sphereonVerifiableCredential) - result = await w3cCredentialService.verifyCredential(agentContext, { credential: w3cVerifiableCredential }) - } else if (format === 'ldp_vc') { - if (format !== 'ldp_vc') throw new Error('Invalid format') - // validate jwt credentials - - w3cVerifiableCredential = JsonTransformer.fromJSON(sphereonVerifiableCredential, W3cJsonLdVerifiableCredential) - result = await w3cCredentialService.verifyCredential(agentContext, { credential: w3cVerifiableCredential }) - } else { - throw new AriesFrameworkError(`Unsupported credential format`) - } - - if (!result.isValid) { - agentContext.config.logger.error('Failed to validate credential', { result }) - throw new AriesFrameworkError(`Failed to validate credential, error = ${result.error?.message ?? 'Unknown'}`) + if (format === OpenIdCredentialFormatProfile.JwtVcJson) { + return { format, types, proof: { jwt: jws, proof_type: 'jwt' } } + } else if (format === OpenIdCredentialFormatProfile.JwtVcJsonLd) { + return { + format, + proof: { jwt: jws, proof_type: 'jwt' }, + credential_definition: { + // TODO: + '@context': ['something'], + types, + }, + } + } else if (format === OpenIdCredentialFormatProfile.SdJwtVc) { + return { format: format, proof: { jwt: jws, proof_type: 'jwt' }, credential_definition: { vct: types[0] } } } - if (equalsIgnoreOrder(w3cVerifiableCredential.type, types) === false) throw new Error('Invalid credential type') - return w3cVerifiableCredential + throw new Error('Unsupported format') } describe('OpenId4VcIssuer', () => { @@ -169,6 +156,7 @@ describe('OpenId4VcIssuer', () => { let holder: Agent let holderKid: string + let holderVerificationMethod: VerificationMethod let holderDid: string let issuerService: OpenId4VcIssuerService @@ -210,6 +198,11 @@ describe('OpenId4VcIssuer', () => { holderDid = holderDidCreateResult.didState.did as string const holderDidKey = DidKey.fromDid(holderDid) holderKid = `${holderDid}#${holderDidKey.key.fingerprint}` + const _holderVerificationMethod = holderDidCreateResult.didState.didDocument?.dereferenceKey(holderKid, [ + 'authentication', + ]) + if (!_holderVerificationMethod) throw new Error('No verification method found') + holderVerificationMethod = _holderVerificationMethod const issuerDidCreateResult = await issuer.dids.create({ method: 'key', @@ -241,7 +234,100 @@ describe('OpenId4VcIssuer', () => { enableNetConnect() }) - it('pre authorized code flow', async () => { + async function handleCredentialResponse( + sphereonVerifiableCredential: SphereonW3cVerifiableCredential, + format: string, + types: string[] + ) { + if (format === 'vc+sd-jwt' && typeof sphereonVerifiableCredential === 'string') { + const r = await holder.modules.sdJwtVc.verify(sphereonVerifiableCredential, { + holderDidUrl: holderKid, + challenge: { verifierDid: holderDid }, + requiredClaimKeys: ['university', 'degree'], + }) + + if (r.validation.isValid) throw new Error('Invalid SdJwtVc received') + return + } + + const w3cCredentialService = holder.context.dependencyManager.resolve(W3cCredentialService) + + let result: W3cVerifyCredentialResult + let w3cVerifiableCredential: W3cVerifiableCredential + + if (typeof sphereonVerifiableCredential === 'string') { + if (format !== 'jwt_vc_json' && format !== 'jwt_vc_json-ld') throw new Error(`Invalid format. ${format}`) + w3cVerifiableCredential = W3cJwtVerifiableCredential.fromSerializedJwt(sphereonVerifiableCredential) + result = await w3cCredentialService.verifyCredential(holder.context, { credential: w3cVerifiableCredential }) + } else if (format === 'ldp_vc') { + if (format !== 'ldp_vc') throw new Error('Invalid format') + // validate jwt credentials + + w3cVerifiableCredential = JsonTransformer.fromJSON(sphereonVerifiableCredential, W3cJsonLdVerifiableCredential) + result = await w3cCredentialService.verifyCredential(holder.context, { credential: w3cVerifiableCredential }) + } else { + throw new AriesFrameworkError(`Unsupported credential format`) + } + + if (!result.isValid) { + holder.context.config.logger.error('Failed to validate credential', { result }) + throw new AriesFrameworkError(`Failed to validate credential, error = ${result.error?.message ?? 'Unknown'}`) + } + + if (equalsIgnoreOrder(w3cVerifiableCredential.type, types) === false) throw new Error('Invalid credential type') + return w3cVerifiableCredential + } + + it('pre authorized code flow (sdjwt)', async () => { + const cNonce = '1234' + const preAuthorizedCode = '1234567890' + + await issuerService.cNonceStateManager.set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) + + const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, userPinRequired: false } + + const result = await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest( + [universityDegreeCredentialSdJwt.id], + { + preAuthorizedCodeFlowConfig, + ...baseCredentialRequestOptions, + } + ) + + expect(result.credentialOfferRequest).toEqual( + 'openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FUniversityDegreeCredentialSdJwt%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%22%7D' + ) + + const { compact } = await issuer.modules.sdJwtVc.create( + { type: 'UniversityDegreeCredential', university: 'innsbruck', degree: 'bachelor' }, + { + holderDidUrl: holderVerificationMethod.id, + issuerDidUrl: issuerVerificationMethod.id, + disclosureFrame: { university: true, degree: true }, + } + ) + + const issueCredentialResponse = await issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ + credential: compact, + verificationMethod: issuerVerificationMethod, + credentialRequest: await createCredentialRequestFromKid(holder.context, { + format: universityDegreeCredentialSdJwt.format, + types: [universityDegreeCredentialSdJwt.credential_definition.vct], + issuerMetadata, + kid: holderKid, + nonce: cNonce, + }), + }) + + const sphereonW3cCredential = issueCredentialResponse.credential + if (!sphereonW3cCredential) throw new Error('No credential found') + + await handleCredentialResponse(sphereonW3cCredential, universityDegreeCredentialSdJwt.format, [ + universityDegreeCredentialSdJwt.credential_definition.vct, + ]) + }) + + it('pre authorized code flow (jwtvcjson)', async () => { const cNonce = '1234' const preAuthorizedCode = '1234567890' @@ -280,12 +366,7 @@ describe('OpenId4VcIssuer', () => { const sphereonW3cCredential = issueCredentialResponse.credential if (!sphereonW3cCredential) throw new Error('No credential found') - await handleCredentialResponse( - holder.context, - sphereonW3cCredential, - openBadgeCredential.format, - openBadgeCredential.types - ) + await handleCredentialResponse(sphereonW3cCredential, openBadgeCredential.format, openBadgeCredential.types) }) it('credential id not in credential supported errors', async () => { @@ -390,7 +471,6 @@ describe('OpenId4VcIssuer', () => { if (!sphereonW3cCredential) throw new Error('No credential found') await handleCredentialResponse( - holder.context, sphereonW3cCredential, universityDegreeCredentialLd.format, universityDegreeCredential.types @@ -479,12 +559,7 @@ describe('OpenId4VcIssuer', () => { const sphereonW3cCredential = issueCredentialResponse.credential if (!sphereonW3cCredential) throw new Error('No credential found') - await handleCredentialResponse( - holder.context, - sphereonW3cCredential, - openBadgeCredential.format, - openBadgeCredential.types - ) + await handleCredentialResponse(sphereonW3cCredential, openBadgeCredential.format, openBadgeCredential.types) }) it('create credential offer and retrieve it from the uri (pre authorized flow)', async () => { @@ -582,12 +657,7 @@ describe('OpenId4VcIssuer', () => { const sphereonW3cCredential = issueCredentialResponse.credential if (!sphereonW3cCredential) throw new Error('No credential found') - await handleCredentialResponse( - holder.context, - sphereonW3cCredential, - openBadgeCredential.format, - openBadgeCredential.types - ) + await handleCredentialResponse(sphereonW3cCredential, openBadgeCredential.format, openBadgeCredential.types) const credential2 = new W3cCredential({ type: universityDegreeCredential.types, @@ -612,7 +682,6 @@ describe('OpenId4VcIssuer', () => { if (!sphereonW3cCredential2) throw new Error('No credential found') await handleCredentialResponse( - holder.context, sphereonW3cCredential2, universityDegreeCredential.format, universityDegreeCredential.types diff --git a/packages/openid4vc-verifier/package.json b/packages/openid4vc-verifier/package.json index d4598453d9..2b5eff2612 100644 --- a/packages/openid4vc-verifier/package.json +++ b/packages/openid4vc-verifier/package.json @@ -26,6 +26,7 @@ "dependencies": { "@aries-framework/askar": "^0.4.2", "@aries-framework/core": "0.4.2", + "@aries-framework/sd-jwt-vc": "^0.4.2", "@sphereon/did-auth-siop": "^0.5.0-unstable.7", "@sphereon/pex": "2.2.0", "@sphereon/pex-models": "^2.1.1", diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierModule.ts b/packages/openid4vc-verifier/src/OpenId4VcVerifierModule.ts index 8a604c1f6c..c275f67ccb 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierModule.ts +++ b/packages/openid4vc-verifier/src/OpenId4VcVerifierModule.ts @@ -27,7 +27,7 @@ export class OpenId4VcVerifierModule implements Module { dependencyManager .resolve(AgentConfig) .logger.warn( - "The '@aries-framework/openid4vc-verifier' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." + "The '@aries-framework/openid4vc-verifier' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages. Multi-Tenancy is not supported. " ) // Register config diff --git a/packages/openid4vc-verifier/tests/openId4vc-verifier-module.test.ts b/packages/openid4vc-verifier/tests/openId4vc-verifier-module.test.ts index ca9f1b99dd..aa2659a35c 100644 --- a/packages/openid4vc-verifier/tests/openId4vc-verifier-module.test.ts +++ b/packages/openid4vc-verifier/tests/openId4vc-verifier-module.test.ts @@ -12,7 +12,7 @@ const dependencyManager = { resolve: jest.fn().mockReturnValue({ logger: { warn: jest.fn() } }), } as unknown as DependencyManager -describe('OpenId4VcIssuerModule', () => { +describe('OpenId4VcVerifierModule', () => { test('registers dependencies on the dependency manager', () => { const openId4VcClientModule = new OpenId4VcVerifierModule({}) openId4VcClientModule.register(dependencyManager) diff --git a/packages/sd-jwt-vc/src/SdJwtVcApi.ts b/packages/sd-jwt-vc/src/SdJwtVcApi.ts index 8091d54c69..2c809ba9eb 100644 --- a/packages/sd-jwt-vc/src/SdJwtVcApi.ts +++ b/packages/sd-jwt-vc/src/SdJwtVcApi.ts @@ -41,6 +41,10 @@ export class SdJwtVcApi { return await this.sdJwtVcService.storeCredential(this.agentContext, sdJwtVcCompact, options) } + public async storeCredential2(sdJwtVcRecord: SdJwtVcRecord): Promise { + return await this.sdJwtVcService.storeCredential2(this.agentContext, sdJwtVcRecord) + } + /** * * Create a compact presentation of the sd-jwt. diff --git a/packages/sd-jwt-vc/src/SdJwtVcService.ts b/packages/sd-jwt-vc/src/SdJwtVcService.ts index e8af00af0d..517617fa10 100644 --- a/packages/sd-jwt-vc/src/SdJwtVcService.ts +++ b/packages/sd-jwt-vc/src/SdJwtVcService.ts @@ -177,6 +177,53 @@ export class SdJwtVcService { } } + public async fromString< + Header extends Record = Record, + Payload extends Record = Record + >( + agentContext: AgentContext, + sdJwtVcCompact: string, + { issuerDidUrl, holderDidUrl }: SdJwtVcReceiveOptions + ): Promise { + const sdJwtVc = SdJwtVc.fromCompact(sdJwtVcCompact) + + if (!sdJwtVc.signature) { + throw new SdJwtVcError('A signature must be included for an sd-jwt-vc') + } + + const { verificationMethod: issuerVerificationMethod } = await this.resolveDidUrl(agentContext, issuerDidUrl) + const issuerKey = getKeyFromVerificationMethod(issuerVerificationMethod) + + const { isSignatureValid } = await sdJwtVc.verify(this.verifier(agentContext, issuerKey)) + + if (!isSignatureValid) { + throw new SdJwtVcError('sd-jwt-vc has an invalid signature from the issuer') + } + + const { verificationMethod: holderVerificiationMethod } = await this.resolveDidUrl(agentContext, holderDidUrl) + const holderKey = getKeyFromVerificationMethod(holderVerificiationMethod) + const holderKeyJwk = getJwkFromKey(holderKey).toJson() + + sdJwtVc.assertClaimInPayload('cnf', { jwk: holderKeyJwk }) + + const sdJwtVcRecord = new SdJwtVcRecord({ + sdJwtVc: { + header: sdJwtVc.header, + payload: sdJwtVc.payload, + signature: sdJwtVc.signature, + disclosures: sdJwtVc.disclosures?.map((d) => d.decoded), + holderDidUrl, + }, + }) + + return sdJwtVcRecord + } + + public async storeCredential2(agentContext: AgentContext, sdJwtVcRecord: SdJwtVcRecord): Promise { + await this.sdJwtVcRepository.save(agentContext, sdJwtVcRecord) + return sdJwtVcRecord + } + public async storeCredential< Header extends Record = Record, Payload extends Record = Record diff --git a/yarn.lock b/yarn.lock index a15b71cfeb..199d84405d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2570,14 +2570,14 @@ integrity sha512-kQpk267uxB19X3X2T1mvNMjyvIEonpNSHrMlK5ZaBU6aZxw7wPbpgKJOjHN3+/GPVpXgAV9soVT2oyHpLkLtyw== "@sphereon/did-auth-siop@^0.5.0-unstable.7": - version "0.5.0-unstable.7" - resolved "https://registry.yarnpkg.com/@sphereon/did-auth-siop/-/did-auth-siop-0.5.0-unstable.7.tgz#3867ffe44f9289ce85f1f41241d98464e0acb4c4" - integrity sha512-Yq/NqrRTin85zkJA1//EzZ8j+hwt1Aw5YK2crN0MRhW6Oo3eoAuhLygCJNvy81hkwgawd8MuP9Bf6rk9vHsFsA== + version "0.5.0-unstable.8" + resolved "https://registry.yarnpkg.com/@sphereon/did-auth-siop/-/did-auth-siop-0.5.0-unstable.8.tgz#5bc9d66d6cedce5c6a39a54059e214c6046a83a9" + integrity sha512-p7tuv9EaGv+U0lj8nBEZYotnUKrySh/rTYrGTH4c+gwJRZqiJCNoiLqfheC4lrMk0dnULd2WuuBe67qlxSaafQ== dependencies: "@astronautlabs/jsonpath" "^1.1.2" "@sphereon/did-uni-client" "^0.6.0" - "@sphereon/pex" "2.2.1-unstable.0" - "@sphereon/pex-models" "^2.1.1" + "@sphereon/pex" "2.2.2" + "@sphereon/pex-models" "^2.1.2" "@sphereon/ssi-types" "^0.17.5" "@sphereon/wellknown-dids-client" "^0.1.3" cross-fetch "^3.1.8" @@ -2599,38 +2599,32 @@ cross-fetch "^3.1.5" did-resolver "^4.1.0" -"@sphereon/oid4vci-client@^0.8.1": +"@sphereon/oid4vci-client@file:../Sphereon/sphereon-oidvci-client-0.8.1": version "0.8.1" - resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-client/-/oid4vci-client-0.8.1.tgz#e05ce5d0f9d5227492b7e5264864cea88a4e10a4" - integrity sha512-NhIxBDTvXRDl7du+z3Mnpm0VkxI1L/r1r4hJzz2Xdh1NKruzk5lkoLbRyq9uCc3rMg/RH8U+nkThrOE0TiwnRg== dependencies: - "@sphereon/oid4vci-common" "0.8.1" + "@sphereon/oid4vci-common" "file:../../../Library/Caches/Yarn/v6/npm-@sphereon-oid4vci-client-0.8.1-301170cf-5685-4f6d-bb5d-6b488d8f7353-1701331237986/node_modules/@sphereon/sphereon-oid4vci-common-0.8.1" "@sphereon/ssi-types" "0.17.2" cross-fetch "^3.1.8" debug "^4.3.4" -"@sphereon/oid4vci-common@0.8.1", "@sphereon/oid4vci-common@^0.8.1": +"@sphereon/oid4vci-common@0.8.1", "@sphereon/oid4vci-common@file:../Sphereon/sphereon-oid4vci-common-0.8.1": version "0.8.1" - resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-common/-/oid4vci-common-0.8.1.tgz#2623f467c3765a96f3330691d6a0946f04360106" - integrity sha512-lKdVjUIkd04Tpt9BLZ+N5mv2uhm4x1Qu5TFrYHBPzXCc0jNMnSWCN3yD0HjfdsW++unLaqAqifvM4iYY5NKSZg== dependencies: "@sphereon/ssi-types" "0.17.2" cross-fetch "^3.1.8" jwt-decode "^3.1.2" -"@sphereon/oid4vci-issuer@^0.8.1": +"@sphereon/oid4vci-issuer@file:../Sphereon/sphereon-oid4vci-issuer-0.8.1": version "0.8.1" - resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-issuer/-/oid4vci-issuer-0.8.1.tgz#7682b133982097d84018b4a4d3a8c903b296f214" - integrity sha512-2IK0XdO3djGPeLFmPI5zYcHRfsZG9ZugQ+VyJUNtEO/nfcwXGxd4pCxzaUxqHRCTXEStCxXRnvf4mntYuhnpIQ== dependencies: "@sphereon/oid4vci-common" "0.8.1" "@sphereon/ssi-types" "0.17.2" uuid "^9.0.0" -"@sphereon/pex-models@^2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@sphereon/pex-models/-/pex-models-2.1.1.tgz#399e529db2a7e3b9abbd7314cdba619ceb6cb758" - integrity sha512-0UX/CMwgiJSxzuBn6SLOTSKkm+uPq3dkNjl8w4EtppXp6zBB4lQMd1mJX7OifX5Bp5vPUfoz7bj2B+yyDtbZww== +"@sphereon/pex-models@^2.1.1", "@sphereon/pex-models@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@sphereon/pex-models/-/pex-models-2.1.2.tgz#e1a0ce16ccc6b32128fc8c2da79d65fc35f6d10f" + integrity sha512-Ec1qZl8tuPd+s6E+ZM7v+HkGkSOjGDMLNN1kqaxAfWpITBYtTLb+d5YvwjvBZ1P2upZ7zwNER97FfW5n/30y2w== "@sphereon/pex@2.2.0": version "2.2.0" @@ -2646,13 +2640,13 @@ nanoid "^3.3.6" string.prototype.matchall "^4.0.8" -"@sphereon/pex@2.2.1-unstable.0": - version "2.2.1-unstable.0" - resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-2.2.1-unstable.0.tgz#58c339f578d487db5e7ac54a79871c58bdbe63ff" - integrity sha512-gAO4+pUSdN+kDHaB1D7oCSn1AZcKOeH3IXv7NdPkf1U4J/sJpLEXA46gsG8SBTX4e45CRGwt8c5F7vPmIwX7XQ== +"@sphereon/pex@2.2.2": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-2.2.2.tgz#3df9ed75281b46f0899256774060ed2ff982fade" + integrity sha512-NkR8iDTC2PSnYsOHlG2M2iOpFTTbzszs2/pL3iK3Dlv9QYLqX7NtPAlmeSwaoVP1NB1ewcs6U1DtemQAD+90yQ== dependencies: "@astronautlabs/jsonpath" "^1.1.2" - "@sphereon/pex-models" "^2.1.1" + "@sphereon/pex-models" "^2.1.2" "@sphereon/ssi-types" "^0.17.5" ajv "^8.12.0" ajv-formats "^2.1.1" From f5f49eed806f02034c7f2c1ad369795d52dce2fa Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 4 Dec 2023 14:31:36 +0100 Subject: [PATCH 077/115] feat: add credential record migration --- .../__tests__/w3cCredentialRecord.test.ts | 63 +++++++++++++++++++ .../migration/updates/0.4-0.5/index.ts | 7 +++ .../updates/0.4-0.5/w3cCredentialRecord.ts | 28 +++++++++ 3 files changed, 98 insertions(+) create mode 100644 packages/core/src/storage/migration/updates/0.4-0.5/__tests__/w3cCredentialRecord.test.ts create mode 100644 packages/core/src/storage/migration/updates/0.4-0.5/index.ts create mode 100644 packages/core/src/storage/migration/updates/0.4-0.5/w3cCredentialRecord.ts diff --git a/packages/core/src/storage/migration/updates/0.4-0.5/__tests__/w3cCredentialRecord.test.ts b/packages/core/src/storage/migration/updates/0.4-0.5/__tests__/w3cCredentialRecord.test.ts new file mode 100644 index 0000000000..31fa8631c4 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.4-0.5/__tests__/w3cCredentialRecord.test.ts @@ -0,0 +1,63 @@ +import { getAgentConfig, getAgentContext, mockFunction } from '../../../../../../tests/helpers' +import { Agent } from '../../../../../agent/Agent' +import { W3cCredentialRecord, W3cJsonLdVerifiableCredential } from '../../../../../modules/vc' +import { Ed25519Signature2018Fixtures } from '../../../../../modules/vc/data-integrity/__tests__/fixtures' +import { JsonTransformer } from '../../../../../utils' +import * as testModule from '../w3cCredentialRecord' + +const agentConfig = getAgentConfig('Migration W3cCredentialRecord 0.4-0.5') +const agentContext = getAgentContext() + +const repository = { + getAll: jest.fn(), + update: jest.fn(), +} + +jest.mock('../../../../../agent/Agent', () => { + return { + Agent: jest.fn(() => ({ + config: agentConfig, + context: agentContext, + dependencyManager: { + resolve: jest.fn(() => repository), + }, + })), + } +}) + +// Mock typed object +const AgentMock = Agent as jest.Mock + +describe('0.4-0.5 | W3cCredentialRecord', () => { + let agent: Agent + + beforeEach(() => { + agent = new AgentMock() + }) + + describe('migrateW3cCredentialRecordToV0_5()', () => { + it('should fetch all w3c credential records and re-save them', async () => { + const records = [ + new W3cCredentialRecord({ + tags: {}, + id: '3b3cf6ca-fa09-4498-b891-e280fbbb7fa7', + credential: JsonTransformer.fromJSON( + Ed25519Signature2018Fixtures.TEST_LD_DOCUMENT_SIGNED, + W3cJsonLdVerifiableCredential + ), + }), + ] + + mockFunction(repository.getAll).mockResolvedValue(records) + + await testModule.migrateW3cCredentialRecordToV0_5(agent) + + expect(repository.getAll).toHaveBeenCalledTimes(1) + expect(repository.getAll).toHaveBeenCalledWith(agent.context) + expect(repository.update).toHaveBeenCalledTimes(1) + + const [, record] = mockFunction(repository.update).mock.calls[0] + expect(record.getTags().claimFormat).toEqual('ldp_vc') + }) + }) +}) diff --git a/packages/core/src/storage/migration/updates/0.4-0.5/index.ts b/packages/core/src/storage/migration/updates/0.4-0.5/index.ts new file mode 100644 index 0000000000..8b1a9428b9 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.4-0.5/index.ts @@ -0,0 +1,7 @@ +import type { BaseAgent } from '../../../../agent/BaseAgent' + +import { migrateW3cCredentialRecordToV0_5 } from './w3cCredentialRecord' + +export async function updateV0_4ToV0_5(agent: Agent): Promise { + await migrateW3cCredentialRecordToV0_5(agent) +} diff --git a/packages/core/src/storage/migration/updates/0.4-0.5/w3cCredentialRecord.ts b/packages/core/src/storage/migration/updates/0.4-0.5/w3cCredentialRecord.ts new file mode 100644 index 0000000000..44adf36171 --- /dev/null +++ b/packages/core/src/storage/migration/updates/0.4-0.5/w3cCredentialRecord.ts @@ -0,0 +1,28 @@ +import type { BaseAgent } from '../../../../agent/BaseAgent' + +import { W3cCredentialRepository } from '../../../../modules/vc/repository' + +/** + * Re-saves the w3c credential records to add the new claimFormat tag. + */ +export async function migrateW3cCredentialRecordToV0_5(agent: Agent) { + agent.config.logger.info('Migration w3c credential records records to storage version 0.5') + + const w3cCredentialRepository = agent.dependencyManager.resolve(W3cCredentialRepository) + + agent.config.logger.debug(`Fetching all w3c credential records from storage`) + const records = await w3cCredentialRepository.getAll(agent.context) + + agent.config.logger.debug(`Found a total of ${records.length} w3c credential records to update.`) + + for (const record of records) { + agent.config.logger.debug( + `Re-saving w3c credential record with id ${record.id} to add claimFormat tag for storage version 0.5` + ) + + // Save updated record + await w3cCredentialRepository.update(agent.context, record) + + agent.config.logger.debug(`Successfully migrated w3c credential record with id ${record.id} to storage version 0.5`) + } +} From 5ff6e3938edfe842fbbfa6985c9341e9c21e7431 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 4 Dec 2023 14:34:01 +0100 Subject: [PATCH 078/115] feat: wrap up sdjwtvc vci work part 1 --- demo-openid/src/Issuer.ts | 4 +- .../tests/openid4vci-holder.e2e.test.ts | 18 +-- .../tests/openid4vp-holder.e2e.test.ts | 12 +- .../src/OpenId4VcIssuerService.ts | 4 +- .../src/OpenId4VcIssuerServiceOptions.ts | 12 +- .../src/issuance/OpenId4VciHolderService.ts | 12 +- .../src/issuance/utils/IssuerMetadataUtils.ts | 10 +- .../router/OpenId4VcIEndpointConfiguration.ts | 38 ++++- .../tests/openid4vc-issuer.e2e.test.ts | 141 ++++++++---------- packages/sd-jwt-vc/src/SdJwtVcOptions.ts | 5 + packages/sd-jwt-vc/src/SdJwtVcService.ts | 22 ++- 11 files changed, 167 insertions(+), 111 deletions(-) diff --git a/demo-openid/src/Issuer.ts b/demo-openid/src/Issuer.ts index 0a21c8fe94..d73dac0ffd 100644 --- a/demo-openid/src/Issuer.ts +++ b/demo-openid/src/Issuer.ts @@ -90,7 +90,7 @@ export class Issuer extends BaseAgent> } public getCredentialRequestToCredentialMapper(): CredentialRequestToCredentialMapper { - return async (credentialRequest, holderDid, holderKid) => { + return async (credentialRequest, { holderDid, holderDidUrl }) => { if ( credentialRequest.format === 'jwt_vc_json' && credentialRequest.types.includes('UniversityDegreeCredential') @@ -119,7 +119,7 @@ export class Issuer extends BaseAgent> const { compact } = await this.agent.modules.sdJwtVc.create( { type: 'UniversityDegreeCredential', university: 'innsbruck', degree: 'bachelor' }, { - holderDidUrl: holderKid, + holderDidUrl, issuerDidUrl: this.kid, disclosureFrame: { university: true, degree: true }, } diff --git a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts index 08bbb5b204..4a343fb4cf 100644 --- a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts @@ -114,7 +114,7 @@ describe('OpenId4VcHolder', () => { issuer = new Agent({ config: { - label: 'OpenId4VcIssuer Test27', + label: 'OpenId4VcIssuer Test28', walletConfig: { id: 'openid4vc-issuer-test27', key: 'openid4vc-issuer-test27' }, }, dependencies: agentDependencies, @@ -123,7 +123,7 @@ describe('OpenId4VcHolder', () => { holder = new Agent({ config: { - label: 'OpenId4VcHolder Test27', + label: 'OpenId4VcHolder Test28', walletConfig: { id: 'openid4vc-holder-test27', key: 'openid4vc-holder-test27' }, }, dependencies: agentDependencies, @@ -626,17 +626,17 @@ describe('OpenId4VcHolder', () => { credentialEndpointConfig: { enabled: true, verificationMethod: issuerVerificationMethod, - credentialRequestToCredentialMapper: async (credentialRequest, _holderDid) => { + credentialRequestToCredentialMapper: async (credentialRequest, metadata) => { if ( credentialRequest.format === 'jwt_vc_json' && credentialRequest.types.includes('OpenBadgeCredential') ) { - if (_holderDid !== holderDid) throw new Error('Invalid holder did') + if (metadata.holderDid !== holderDid) throw new Error('Invalid holder did') return new W3cCredential({ type: openBadgeCredential.types, issuer: new W3cIssuer({ id: issuerDid }), - credentialSubject: new W3cCredentialSubject({ id: _holderDid }), + credentialSubject: new W3cCredentialSubject({ id: metadata.holderDid }), issuanceDate: w3cDate(Date.now()), }) } @@ -723,18 +723,18 @@ describe('OpenId4VcHolder', () => { credentialEndpointConfig: { enabled: true, verificationMethod: issuerVerificationMethod, - credentialRequestToCredentialMapper: async (credentialRequest, _holderDid, holderKid) => { + credentialRequestToCredentialMapper: async (credentialRequest, metadata) => { if ( credentialRequest.format === 'vc+sd-jwt' && credentialRequest.credential_definition.vct === 'UniversityDegreeCredential' ) { - if (_holderDid !== holderDid) throw new Error('Invalid holder did') + if (metadata.holderDid !== holderDid) throw new Error('Invalid holder did') const { compact } = await issuer.modules.sdJwtVc.create( { type: 'UniversityDegreeCredential', university: 'innsbruck', degree: 'bachelor' }, { - holderDidUrl: holderKid, - issuerDidUrl: issuerVerificationMethod.id, + holderDidUrl: metadata.holderDidUrl, + issuerDidUrl: issuerKid, disclosureFrame: { university: true, degree: true }, } ) diff --git a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts index 9be2c12859..d7090ae3c8 100644 --- a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts +++ b/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts @@ -129,10 +129,10 @@ describe('OpenId4VcHolder | OpenID4VP', () => { verifierApp = express() verifier = new Agent({ config: { - label: 'OpenId4VcRp OpenID4VP Test42', + label: 'OpenId4VcRp OpenID4VP Test43', walletConfig: { - id: 'openid4vc-rp-openid4vp-test42', - key: 'openid4vc-rp-openid4vp-test42', + id: 'openid4vc-rp-openid4vp-test43', + key: 'openid4vc-rp-openid4vp-test43', }, }, dependencies: agentDependencies, @@ -141,10 +141,10 @@ describe('OpenId4VcHolder | OpenID4VP', () => { holder = new Agent({ config: { - label: 'OpenId4VcOp OpenID4VP Test42', + label: 'OpenId4VcOp OpenID4VP Test43', walletConfig: { - id: 'openid4vc-op-openid4vp-test42', - key: 'openid4vc-op-openid4vp-test42', + id: 'openid4vc-op-openid4vp-test43', + key: 'openid4vc-op-openid4vp-test43', }, }, dependencies: agentDependencies, diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts index e4d27042ba..32773b8280 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts @@ -470,9 +470,9 @@ export class OpenId4VcIssuerService { configureAccessTokenEndpoint(agentContext, router, this.logger, { ...endpointConfig.accessTokenEndpointConfig, issuerMetadata: this.issuerMetadata, - cNonceStateManager: this.cNonceStateManager, cNonceExpiresIn: this.openId4VcIssuerModuleConfig.cNonceExpiresIn, tokenExpiresIn: this.openId4VcIssuerModuleConfig.tokenExpiresIn, + cNonceStateManager: this.cNonceStateManager, credentialOfferSessionManager: this.credentialOfferSessionManager, }) } @@ -481,6 +481,8 @@ export class OpenId4VcIssuerService { configureCredentialEndpoint(agentContext, router, this.logger, { ...endpointConfig.credentialEndpointConfig, issuerMetadata: this.issuerMetadata, + cNonceStateManager: this.cNonceStateManager, + credentialOfferSessionManager: this.credentialOfferSessionManager, createIssueCredentialResponse: (agentContext, options) => { const issuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService) return issuerService.createIssueCredentialResponse(agentContext, options) diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts index 08320b6760..53435ce427 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts @@ -1,7 +1,9 @@ import type { VerificationMethod, W3cCredential } from '@aries-framework/core' import type { + CNonceState, CredentialOfferFormat, CredentialOfferPayloadV1_0_11, + CredentialOfferSession, CredentialRequestV1_0_11, CredentialSupported, MetadataDisplay, @@ -102,10 +104,16 @@ export interface AccessTokenEndpointConfig { preAuthorizedCodeExpirationDuration: number } +export type CredentialRequestToCredentialMetadata = { + holderDid: string + holderDidUrl: string + cNonceState: CNonceState + credentialOfferSession: CredentialOfferSession +} + export type CredentialRequestToCredentialMapper = ( credentialRequest: CredentialRequestV1_0_11, - holderDid: string, - holderDidUrl: string + metadata: CredentialRequestToCredentialMetadata ) => Promise export interface CredentialEndpointConfig { diff --git a/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderService.ts b/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderService.ts index 06933fd30f..7e223da6ab 100644 --- a/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderService.ts +++ b/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderService.ts @@ -306,8 +306,8 @@ export class OpenId4VciHolderService { const credential_definition = { '@context': credentialWithMetadata.credentialSupported['@context'], - types, credentialSubject: credentialWithMetadata.credentialSupported.credentialSubject, + types, } return { type, format, locations, credential_definition } @@ -539,7 +539,6 @@ export class OpenId4VciHolderService { const credentialRecord = await this.handleCredentialResponse(agentContext, credentialResponse, { verifyCredentialStatus: verifyCredentialStatus ?? false, holderDidUrl: verificationMethod.id, - issuerDidUrl: verificationMethod.controller, // TODO: how to figure this out? }) this.logger.debug('Full credential', credentialRecord) @@ -712,7 +711,7 @@ export class OpenId4VciHolderService { private async handleCredentialResponse( agentContext: AgentContext, credentialResponse: OpenIDResponse, - options: { verifyCredentialStatus: boolean; holderDidUrl: string; issuerDidUrl: string } + options: { verifyCredentialStatus: boolean; holderDidUrl: string } ): Promise { const { verifyCredentialStatus, holderDidUrl } = options this.logger.debug('Credential request response', credentialResponse) @@ -735,11 +734,8 @@ export class OpenId4VciHolderService { }, but the credential is not a string. ${JSON.stringify(credentialResponse.successBody.credential)}` ) - // TODO const sdJwtVcRecord = await sdJwtVcService.fromString(agentContext, credentialResponse.successBody.credential, { holderDidUrl, - issuerDidUrl: - 'did:key:z6MktiQQEqm2yapXBDt1WEVB3dqgvyzi96FuFANYmrgTrKV9#z6MktiQQEqm2yapXBDt1WEVB3dqgvyzi96FuFANYmrgTrKV9', }) return sdJwtVcRecord @@ -747,11 +743,11 @@ export class OpenId4VciHolderService { let credential: W3cVerifiableCredential let result: W3cVerifyCredentialResult - if (format === OpenIdCredentialFormatProfile.JwtVcJson || format === OpenIdCredentialFormatProfile.JwtVcJsonLd) { + if (format === OpenIdCredentialFormatProfile.LdpVc || format === OpenIdCredentialFormatProfile.JwtVcJsonLd) { // validate json-ld credentials credential = JsonTransformer.fromJSON(credentialResponse.successBody.credential, W3cJsonLdVerifiableCredential) result = await this.w3cCredentialService.verifyCredential(agentContext, { credential, verifyCredentialStatus }) - } else if (format === OpenIdCredentialFormatProfile.LdpVc) { + } else if (format === OpenIdCredentialFormatProfile.JwtVcJson) { // validate jwt credentials credential = W3cJwtVerifiableCredential.fromSerializedJwt(credentialResponse.successBody.credential as string) result = await this.w3cCredentialService.verifyCredential(agentContext, { credential, verifyCredentialStatus }) diff --git a/packages/openid4vc-issuer/src/issuance/utils/IssuerMetadataUtils.ts b/packages/openid4vc-issuer/src/issuance/utils/IssuerMetadataUtils.ts index 1441160066..dd2b4f2dc4 100644 --- a/packages/openid4vc-issuer/src/issuance/utils/IssuerMetadataUtils.ts +++ b/packages/openid4vc-issuer/src/issuance/utils/IssuerMetadataUtils.ts @@ -22,7 +22,7 @@ import { AriesFrameworkError } from '@aries-framework/core' import { MetadataClient } from '@sphereon/oid4vci-client' import { OpenId4VCIVersion } from '@sphereon/oid4vci-common' -import { getUniformFormat } from './Formats' +import { getUniformFormat, getFormatForVersion } from './Formats' import { OpenIdCredentialFormatProfile } from './claimFormatMapping' /** @@ -93,7 +93,8 @@ export function getOfferedCredentialsWithMetadata( const foundSupportedCredentials = supportedCredentials.filter( (supportedCredential) => supportedCredential.id === offeredCredential || - supportedCredential.id === `${offeredCredential}-${supportedCredential.format}` + supportedCredential.id === + `${offeredCredential}-${getFormatForVersion(supportedCredential.format, OpenId4VCIVersion.VER_1_0_08)}` ) // Make sure the issuer metadata includes the offered credential. @@ -228,7 +229,10 @@ export function credentialSupportedV8ToV11( credentialSubject: supportedV8.claims, id, } - } else if (v11Format === OpenIdCredentialFormatProfile.JwtVcJsonLd) { + } else if ( + v11Format === OpenIdCredentialFormatProfile.JwtVcJsonLd || + v11Format === OpenIdCredentialFormatProfile.LdpVc + ) { credentialSupported = { format: v11Format, display: supportedV8.display, diff --git a/packages/openid4vc-issuer/src/router/OpenId4VcIEndpointConfiguration.ts b/packages/openid4vc-issuer/src/router/OpenId4VcIEndpointConfiguration.ts index d7472d49bf..2386a46fa6 100644 --- a/packages/openid4vc-issuer/src/router/OpenId4VcIEndpointConfiguration.ts +++ b/packages/openid4vc-issuer/src/router/OpenId4VcIEndpointConfiguration.ts @@ -84,6 +84,8 @@ export function configureAccessTokenEndpoint( export interface InternalCredentialEndpointConfig extends CredentialEndpointConfig { issuerMetadata: IssuerMetadata + cNonceStateManager: IStateManager + credentialOfferSessionManager: IStateManager createIssueCredentialResponse: ( agentContext: AgentContext, options: CreateIssueCredentialResponseOptions @@ -96,7 +98,13 @@ export function configureCredentialEndpoint( logger: Logger, config: InternalCredentialEndpointConfig ): void { - const { issuerMetadata, credentialRequestToCredentialMapper, verificationMethod } = config + const { + issuerMetadata, + credentialRequestToCredentialMapper, + verificationMethod, + cNonceStateManager, + credentialOfferSessionManager, + } = config const { path, url } = getEndpointMetadata(issuerMetadata.credentialEndpoint, issuerMetadata.credentialIssuer) logger.info(`[OID4VCI] Token endpoint running at '${url.toString()}'.`) @@ -117,7 +125,33 @@ export function configureCredentialEndpoint( const didDocument = await didsApi.resolveDidDocument(kid) const holderDid = didDocument.id - const credential = await credentialRequestToCredentialMapper(credentialRequest, holderDid, kid) + const requestNonce = jwt.payload.additionalClaims.nonce + if (!requestNonce || typeof requestNonce !== 'string') { + throw new AriesFrameworkError(`Received a credential request without a valid nonce. ${requestNonce}`) + } + + const cNonceState = await cNonceStateManager.get(requestNonce) + + const credentialOfferSessionId = cNonceState?.preAuthorizedCode ?? cNonceState?.issuerState + + if (!cNonceState || !credentialOfferSessionId) { + throw new AriesFrameworkError( + `Request nonce '${requestNonce}' is not associated with a preAuthorizedCode or issuerState.` + ) + } + + const credentialOfferSession = await credentialOfferSessionManager.get(credentialOfferSessionId) + if (!credentialOfferSession) + throw new AriesFrameworkError( + `Credential offer session for request nonce '${requestNonce}' with id '${credentialOfferSessionId}' not found.` + ) + + const credential = await credentialRequestToCredentialMapper(credentialRequest, { + holderDid, + holderDidUrl: kid, + cNonceState, + credentialOfferSession, + }) const issueCredentialResponse = await config.createIssueCredentialResponse(agentContext, { credentialRequest, diff --git a/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts b/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts index ebddf5120f..83d34e8673 100644 --- a/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts +++ b/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts @@ -33,32 +33,35 @@ import { getJwkFromKey, getKeyFromVerificationMethod, w3cDate, + equalsIgnoreOrder, } from '@aries-framework/core' import { agentDependencies } from '@aries-framework/node' +import { SdJwtVcModule, SdJwtVcService } from '@aries-framework/sd-jwt-vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { cleanAll, enableNetConnect } from 'nock' -import { equalsIgnoreOrder } from '../../core/src/utils/deepEquality' -import { SdJwtVcModule } from '../../sd-jwt-vc/src/SdJwtVcModule' import { OpenIdCredentialFormatProfile, OpenId4VcIssuerModule, OpenId4VcIssuerService } from '../src' +type CredentialSupportedWithId = CredentialSupported & { id: string } + const openBadgeCredential = { id: 'https://openid4vc-issuer.com/credentials/OpenBadgeCredential', format: OpenIdCredentialFormatProfile.JwtVcJson, types: ['VerifiableCredential', 'OpenBadgeCredential'], -} satisfies CredentialSupported & { id: string } +} satisfies CredentialSupportedWithId const universityDegreeCredential = { id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredential', format: OpenIdCredentialFormatProfile.JwtVcJson, types: ['VerifiableCredential', 'UniversityDegreeCredential'], -} satisfies CredentialSupported & { id: string } +} satisfies CredentialSupportedWithId const universityDegreeCredentialLd = { id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredentialLd', - format: OpenIdCredentialFormatProfile.JwtVcJson, + format: OpenIdCredentialFormatProfile.JwtVcJsonLd, + '@context': [], types: ['VerifiableCredential', 'UniversityDegreeCredential'], -} satisfies CredentialSupported & { id: string } +} satisfies CredentialSupportedWithId const universityDegreeCredentialSdJwt = { id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredentialSdJwt', @@ -66,7 +69,7 @@ const universityDegreeCredentialSdJwt = { credential_definition: { vct: 'UniversityDegreeCredential', }, -} satisfies CredentialSupported & { id: string } +} satisfies CredentialSupportedWithId const baseCredentialRequestOptions = { scheme: 'openid-credential-offer', @@ -88,18 +91,17 @@ const modules = { const jwsService = new JwsService() -const createCredentialRequestFromKid = async ( +const createCredentialRequest = async ( agentContext: AgentContext, options: { issuerMetadata: IssuerMetadata - format: OpenIdCredentialFormatProfile - types: string[] + credentialSupported: CredentialSupportedWithId nonce: string kid: string clientId?: string // use with the authorization code flow, } ): Promise => { - const { format, types, kid, nonce, issuerMetadata, clientId } = options + const { credentialSupported, kid, nonce, issuerMetadata, clientId } = options const aud = issuerMetadata.credentialIssuer @@ -130,20 +132,19 @@ const createCredentialRequestFromKid = async ( key, }) - if (format === OpenIdCredentialFormatProfile.JwtVcJson) { - return { format, types, proof: { jwt: jws, proof_type: 'jwt' } } - } else if (format === OpenIdCredentialFormatProfile.JwtVcJsonLd) { + if (credentialSupported.format === OpenIdCredentialFormatProfile.JwtVcJson) { + return { ...credentialSupported, proof: { jwt: jws, proof_type: 'jwt' } } + } else if ( + credentialSupported.format === OpenIdCredentialFormatProfile.JwtVcJsonLd || + credentialSupported.format === OpenIdCredentialFormatProfile.LdpVc + ) { return { - format, + format: credentialSupported.format, + credential_definition: { '@context': credentialSupported['@context'], types: credentialSupported.types }, proof: { jwt: jws, proof_type: 'jwt' }, - credential_definition: { - // TODO: - '@context': ['something'], - types, - }, } - } else if (format === OpenIdCredentialFormatProfile.SdJwtVc) { - return { format: format, proof: { jwt: jws, proof_type: 'jwt' }, credential_definition: { vct: types[0] } } + } else if (credentialSupported.format === OpenIdCredentialFormatProfile.SdJwtVc) { + return { ...credentialSupported, proof: { jwt: jws, proof_type: 'jwt' } } } throw new Error('Unsupported format') @@ -235,18 +236,15 @@ describe('OpenId4VcIssuer', () => { }) async function handleCredentialResponse( + agentContext: AgentContext, sphereonVerifiableCredential: SphereonW3cVerifiableCredential, - format: string, - types: string[] + credentialSupported: CredentialSupportedWithId ) { - if (format === 'vc+sd-jwt' && typeof sphereonVerifiableCredential === 'string') { - const r = await holder.modules.sdJwtVc.verify(sphereonVerifiableCredential, { - holderDidUrl: holderKid, - challenge: { verifierDid: holderDid }, - requiredClaimKeys: ['university', 'degree'], - }) + if (credentialSupported.format === 'vc+sd-jwt' && typeof sphereonVerifiableCredential === 'string') { + const sdJwtVcService = holder.context.dependencyManager.resolve(SdJwtVcService) - if (r.validation.isValid) throw new Error('Invalid SdJwtVc received') + // this throws if invalid + await sdJwtVcService.fromString(agentContext, sphereonVerifiableCredential, { holderDidUrl: holderKid }) return } @@ -256,11 +254,13 @@ describe('OpenId4VcIssuer', () => { let w3cVerifiableCredential: W3cVerifiableCredential if (typeof sphereonVerifiableCredential === 'string') { - if (format !== 'jwt_vc_json' && format !== 'jwt_vc_json-ld') throw new Error(`Invalid format. ${format}`) + if (credentialSupported.format !== 'jwt_vc_json' && credentialSupported.format !== 'jwt_vc_json-ld') { + throw new Error(`Invalid format. ${credentialSupported.format}`) + } w3cVerifiableCredential = W3cJwtVerifiableCredential.fromSerializedJwt(sphereonVerifiableCredential) result = await w3cCredentialService.verifyCredential(holder.context, { credential: w3cVerifiableCredential }) - } else if (format === 'ldp_vc') { - if (format !== 'ldp_vc') throw new Error('Invalid format') + } else if (credentialSupported.format === 'ldp_vc') { + if (credentialSupported.format !== 'ldp_vc') throw new Error('Invalid format') // validate jwt credentials w3cVerifiableCredential = JsonTransformer.fromJSON(sphereonVerifiableCredential, W3cJsonLdVerifiableCredential) @@ -274,11 +274,13 @@ describe('OpenId4VcIssuer', () => { throw new AriesFrameworkError(`Failed to validate credential, error = ${result.error?.message ?? 'Unknown'}`) } - if (equalsIgnoreOrder(w3cVerifiableCredential.type, types) === false) throw new Error('Invalid credential type') + if (equalsIgnoreOrder(w3cVerifiableCredential.type, credentialSupported.types) === false) { + throw new Error('Invalid credential type') + } return w3cVerifiableCredential } - it('pre authorized code flow (sdjwt)', async () => { + it('pre authorized code flow (sdjwtvc)', async () => { const cNonce = '1234' const preAuthorizedCode = '1234567890' @@ -298,6 +300,7 @@ describe('OpenId4VcIssuer', () => { 'openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FUniversityDegreeCredentialSdJwt%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%22%7D' ) + // TODO: const { compact } = await issuer.modules.sdJwtVc.create( { type: 'UniversityDegreeCredential', university: 'innsbruck', degree: 'bachelor' }, { @@ -310,9 +313,8 @@ describe('OpenId4VcIssuer', () => { const issueCredentialResponse = await issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ credential: compact, verificationMethod: issuerVerificationMethod, - credentialRequest: await createCredentialRequestFromKid(holder.context, { - format: universityDegreeCredentialSdJwt.format, - types: [universityDegreeCredentialSdJwt.credential_definition.vct], + credentialRequest: await createCredentialRequest(holder.context, { + credentialSupported: universityDegreeCredentialSdJwt, issuerMetadata, kid: holderKid, nonce: cNonce, @@ -322,9 +324,7 @@ describe('OpenId4VcIssuer', () => { const sphereonW3cCredential = issueCredentialResponse.credential if (!sphereonW3cCredential) throw new Error('No credential found') - await handleCredentialResponse(sphereonW3cCredential, universityDegreeCredentialSdJwt.format, [ - universityDegreeCredentialSdJwt.credential_definition.vct, - ]) + await handleCredentialResponse(holder.context, sphereonW3cCredential, universityDegreeCredentialSdJwt) }) it('pre authorized code flow (jwtvcjson)', async () => { @@ -354,9 +354,8 @@ describe('OpenId4VcIssuer', () => { const issueCredentialResponse = await issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ credential, verificationMethod: issuerVerificationMethod, - credentialRequest: await createCredentialRequestFromKid(holder.context, { - format: openBadgeCredential.format, - types: openBadgeCredential.types, + credentialRequest: await createCredentialRequest(holder.context, { + credentialSupported: openBadgeCredential, issuerMetadata, kid: holderKid, nonce: cNonce, @@ -366,7 +365,7 @@ describe('OpenId4VcIssuer', () => { const sphereonW3cCredential = issueCredentialResponse.credential if (!sphereonW3cCredential) throw new Error('No credential found') - await handleCredentialResponse(sphereonW3cCredential, openBadgeCredential.format, openBadgeCredential.types) + await handleCredentialResponse(holder.context, sphereonW3cCredential, openBadgeCredential) }) it('credential id not in credential supported errors', async () => { @@ -417,9 +416,8 @@ describe('OpenId4VcIssuer', () => { issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ credential, verificationMethod: issuerVerificationMethod, - credentialRequest: await createCredentialRequestFromKid(holder.context, { - format: openBadgeCredential.format, - types: openBadgeCredential.types, + credentialRequest: await createCredentialRequest(holder.context, { + credentialSupported: openBadgeCredential, issuerMetadata, kid: holderKid, nonce: cNonce, @@ -458,9 +456,8 @@ describe('OpenId4VcIssuer', () => { const issueCredentialResponse = await issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ credential, verificationMethod: issuerVerificationMethod, - credentialRequest: await createCredentialRequestFromKid(holder.context, { - format: universityDegreeCredentialLd.format, - types: universityDegreeCredentialLd.types, + credentialRequest: await createCredentialRequest(holder.context, { + credentialSupported: universityDegreeCredentialLd, issuerMetadata, kid: holderKid, nonce: cNonce, @@ -470,11 +467,7 @@ describe('OpenId4VcIssuer', () => { const sphereonW3cCredential = issueCredentialResponse.credential if (!sphereonW3cCredential) throw new Error('No credential found') - await handleCredentialResponse( - sphereonW3cCredential, - universityDegreeCredentialLd.format, - universityDegreeCredential.types - ) + await handleCredentialResponse(holder.context, sphereonW3cCredential, universityDegreeCredentialLd) }) it('requesting non offered credential errors', async () => { @@ -508,9 +501,12 @@ describe('OpenId4VcIssuer', () => { issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ credential, verificationMethod: issuerVerificationMethod, - credentialRequest: await createCredentialRequestFromKid(holder.context, { - format: openBadgeCredential.format, - types: universityDegreeCredential.types, + credentialRequest: await createCredentialRequest(holder.context, { + credentialSupported: { + id: 'someid', + format: openBadgeCredential.format, + types: universityDegreeCredential.types, + }, issuerMetadata, kid: holderKid, nonce: cNonce, @@ -546,9 +542,8 @@ describe('OpenId4VcIssuer', () => { const issueCredentialResponse = await issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ credential, verificationMethod: issuerVerificationMethod, - credentialRequest: await createCredentialRequestFromKid(holder.context, { - format: openBadgeCredential.format, - types: openBadgeCredential.types, + credentialRequest: await createCredentialRequest(holder.context, { + credentialSupported: openBadgeCredential, issuerMetadata, kid: holderKid, nonce: cNonce, @@ -559,7 +554,7 @@ describe('OpenId4VcIssuer', () => { const sphereonW3cCredential = issueCredentialResponse.credential if (!sphereonW3cCredential) throw new Error('No credential found') - await handleCredentialResponse(sphereonW3cCredential, openBadgeCredential.format, openBadgeCredential.types) + await handleCredentialResponse(holder.context, sphereonW3cCredential, openBadgeCredential) }) it('create credential offer and retrieve it from the uri (pre authorized flow)', async () => { @@ -645,9 +640,8 @@ describe('OpenId4VcIssuer', () => { const issueCredentialResponse = await issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ credential, verificationMethod: issuerVerificationMethod, - credentialRequest: await createCredentialRequestFromKid(holder.context, { - format: openBadgeCredential.format, - types: openBadgeCredential.types, + credentialRequest: await createCredentialRequest(holder.context, { + credentialSupported: openBadgeCredential, issuerMetadata, kid: holderKid, nonce: cNonce, @@ -657,7 +651,7 @@ describe('OpenId4VcIssuer', () => { const sphereonW3cCredential = issueCredentialResponse.credential if (!sphereonW3cCredential) throw new Error('No credential found') - await handleCredentialResponse(sphereonW3cCredential, openBadgeCredential.format, openBadgeCredential.types) + await handleCredentialResponse(holder.context, sphereonW3cCredential, openBadgeCredential) const credential2 = new W3cCredential({ type: universityDegreeCredential.types, @@ -669,9 +663,8 @@ describe('OpenId4VcIssuer', () => { const issueCredentialResponse2 = await issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ credential: credential2, verificationMethod: issuerVerificationMethod, - credentialRequest: await createCredentialRequestFromKid(holder.context, { - format: universityDegreeCredential.format, - types: universityDegreeCredential.types, + credentialRequest: await createCredentialRequest(holder.context, { + credentialSupported: universityDegreeCredential, issuerMetadata, kid: holderKid, nonce: issueCredentialResponse.c_nonce ?? cNonce, @@ -681,10 +674,6 @@ describe('OpenId4VcIssuer', () => { const sphereonW3cCredential2 = issueCredentialResponse2.credential if (!sphereonW3cCredential2) throw new Error('No credential found') - await handleCredentialResponse( - sphereonW3cCredential2, - universityDegreeCredential.format, - universityDegreeCredential.types - ) + await handleCredentialResponse(holder.context, sphereonW3cCredential2, universityDegreeCredential) }) }) diff --git a/packages/sd-jwt-vc/src/SdJwtVcOptions.ts b/packages/sd-jwt-vc/src/SdJwtVcOptions.ts index d7e2ea7ece..f247b73215 100644 --- a/packages/sd-jwt-vc/src/SdJwtVcOptions.ts +++ b/packages/sd-jwt-vc/src/SdJwtVcOptions.ts @@ -9,6 +9,11 @@ export type SdJwtVcCreateOptions = Recor hashingAlgorithm?: HashName } +export type SdJwtVcFromStringOptions = { + issuerDidUrl?: string + holderDidUrl: string +} + export type SdJwtVcReceiveOptions = { issuerDidUrl: string holderDidUrl: string diff --git a/packages/sd-jwt-vc/src/SdJwtVcService.ts b/packages/sd-jwt-vc/src/SdJwtVcService.ts index 517617fa10..bec1ebdad6 100644 --- a/packages/sd-jwt-vc/src/SdJwtVcService.ts +++ b/packages/sd-jwt-vc/src/SdJwtVcService.ts @@ -2,6 +2,7 @@ import type { SdJwtVcCreateOptions, SdJwtVcPresentOptions, SdJwtVcReceiveOptions, + SdJwtVcFromStringOptions, SdJwtVcVerifyOptions, } from './SdJwtVcOptions' import type { AgentContext, JwkJson, Query } from '@aries-framework/core' @@ -183,15 +184,32 @@ export class SdJwtVcService { >( agentContext: AgentContext, sdJwtVcCompact: string, - { issuerDidUrl, holderDidUrl }: SdJwtVcReceiveOptions + { issuerDidUrl, holderDidUrl }: SdJwtVcFromStringOptions ): Promise { const sdJwtVc = SdJwtVc.fromCompact(sdJwtVcCompact) + let url: string | undefined + if (issuerDidUrl) { + url = issuerDidUrl + } else { + const iss = sdJwtVc.payload?.iss + if (typeof iss === 'string' && iss.startsWith('did')) { + const kid = sdJwtVc.header?.kid + if (!kid || typeof kid !== 'string') throw new SdJwtVcError(`Missing 'kid' in header of SdJwtVc.`) + if (kid.startsWith('did:')) url = kid + else url = `${iss}#${kid}` + } else if (typeof iss === 'string' && URL.canParse(iss)) { + throw new SdJwtVcError(`Resolving the key material from the 'iss' claim is not supported yet.`) + } else { + throw new SdJwtVcError(`Invalid iss claim '${iss}' in SdJwtVc.`) + } + } + if (!sdJwtVc.signature) { throw new SdJwtVcError('A signature must be included for an sd-jwt-vc') } - const { verificationMethod: issuerVerificationMethod } = await this.resolveDidUrl(agentContext, issuerDidUrl) + const { verificationMethod: issuerVerificationMethod } = await this.resolveDidUrl(agentContext, url) const issuerKey = getKeyFromVerificationMethod(issuerVerificationMethod) const { isSignatureValid } = await sdJwtVc.verify(this.verifier(agentContext, issuerKey)) From e685785e9223996f364d229fa1a0da0043993433 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 5 Dec 2023 14:24:29 +0100 Subject: [PATCH 079/115] feat: remove sd-jwt-vc dependency --- demo-openid/src/Holder.ts | 2 +- packages/core/src/plugins/index.ts | 1 + packages/core/src/plugins/utils.ts | 34 ++ packages/openid4vc-issuer/package.json | 2 +- .../src/OpenId4VcIssuerService.ts | 7 +- .../src/issuance/OpenId4VciHolderService.ts | 46 +-- .../tests/openid4vc-issuer.e2e.test.ts | 8 +- packages/openid4vc-verifier/package.json | 2 +- packages/sd-jwt-vc/package.json | 1 + packages/sd-jwt-vc/src/SdJwtVcApi.ts | 18 +- packages/sd-jwt-vc/src/SdJwtVcOptions.ts | 7 +- packages/sd-jwt-vc/src/SdJwtVcService.ts | 54 +--- .../src/__tests__/SdJwtVcService.test.ts | 59 +++- packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts | 11 +- yarn.lock | 305 +++++++++++++++++- 15 files changed, 415 insertions(+), 142 deletions(-) create mode 100644 packages/core/src/plugins/utils.ts diff --git a/demo-openid/src/Holder.ts b/demo-openid/src/Holder.ts index ceeeede4b3..d922908c2a 100644 --- a/demo-openid/src/Holder.ts +++ b/demo-openid/src/Holder.ts @@ -54,7 +54,7 @@ export class Holder extends BaseAgent> if (record.type === 'W3cCredentialRecord') { return this.agent.w3cCredentials.storeCredential({ credential: record.credential }) } - return this.agent.modules.sdJwtVc.storeCredential2(record) + return this.agent.modules.sdJwtVc.storeCredential(record) }) ) diff --git a/packages/core/src/plugins/index.ts b/packages/core/src/plugins/index.ts index faee88c0f1..bf419032f6 100644 --- a/packages/core/src/plugins/index.ts +++ b/packages/core/src/plugins/index.ts @@ -1,3 +1,4 @@ export * from './DependencyManager' export * from './Module' +export * from './utils' export { inject, injectable, Disposable, injectAll } from 'tsyringe' diff --git a/packages/core/src/plugins/utils.ts b/packages/core/src/plugins/utils.ts new file mode 100644 index 0000000000..34ad85a7b9 --- /dev/null +++ b/packages/core/src/plugins/utils.ts @@ -0,0 +1,34 @@ +import type { ApiModule, Module } from './Module' +import type { AgentContext } from '../agent' + +export function getRegisteredModuleByInstance( + agentContext: AgentContext, + moduleType: { new (...args: unknown[]): M } +): M | undefined { + const module = Object.values(agentContext.dependencyManager.registeredModules).find( + (module): module is M => module instanceof moduleType + ) + + return module +} + +export function getRegisteredModuleByName( + agentContext: AgentContext, + constructorName: string +): M | undefined { + const module = Object.values(agentContext.dependencyManager.registeredModules).find( + (module): module is M => module.constructor.name === constructorName + ) + + return module +} + +export function getApiForModuleByName( + agentContext: AgentContext, + constructorName: string +): InstanceType | undefined { + const module = getRegisteredModuleByName(agentContext, constructorName) + if (!module || !module.api) return undefined + + return agentContext.dependencyManager.resolve(module.api) as InstanceType +} diff --git a/packages/openid4vc-issuer/package.json b/packages/openid4vc-issuer/package.json index fb0ac45ff7..45767d07de 100644 --- a/packages/openid4vc-issuer/package.json +++ b/packages/openid4vc-issuer/package.json @@ -26,12 +26,12 @@ "dependencies": { "@aries-framework/askar": "^0.4.2", "@aries-framework/core": "0.4.2", - "@aries-framework/sd-jwt-vc": "^0.4.2", "@sphereon/ssi-types": "^0.17.5", "body-parser": "^1.20.2" }, "devDependencies": { "@aries-framework/node": "^0.4.2", + "@aries-framework/sd-jwt-vc": "^0.4.2", "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", "@types/body-parser": "^1.19.5", "@types/express": "^4.17.21", diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts b/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts index 32773b8280..7ec716dc6e 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts +++ b/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts @@ -397,9 +397,10 @@ export class OpenId4VcIssuerService { ) } - const issuedCredentialMatchesRequest = offeredCredentialsMatchingRequest.find( - (offeredCredential) => offeredCredential.types === credential.type - ) + // TODO: Valide SdJwtVc Types + const issuedCredentialMatchesRequest = offeredCredentialsMatchingRequest.find((offeredCredential) => { + return equalsIgnoreOrder(offeredCredential.types, credential.type) + }) if (!issuedCredentialMatchesRequest) { throw new AriesFrameworkError('The credential to be issued does not match the request.') diff --git a/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderService.ts b/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderService.ts index 7e223da6ab..b496d9e504 100644 --- a/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderService.ts +++ b/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderService.ts @@ -6,6 +6,7 @@ import type { W3cVerifiableCredential, W3cVerifyCredentialResult, } from '@aries-framework/core' +import type { SdJwtVcRecord, SdJwtVcModule } from '@aries-framework/sd-jwt-vc' import type { AccessTokenResponse, CredentialOfferPayloadV1_0_11, @@ -41,8 +42,8 @@ import { parseDid, equalsIgnoreOrder, getJwkClassFromKeyType, + getApiForModuleByName, } from '@aries-framework/core' -import { SdJwtVcService, type SdJwtVcRecord } from '@aries-framework/sd-jwt-vc' import { AccessTokenClient, CredentialOfferClient, @@ -254,33 +255,6 @@ export class OpenId4VciHolderService { } } - private getScopeForOfferedCredential( - credentialWithMetadata: OfferedCredentialWithMetadata, - version: OpenId4VCIVersion - ): string | undefined { - const { format, offerType } = credentialWithMetadata - - // TODO: sdjwt - if (version <= OpenId4VCIVersion.VER_1_0_11) { - return undefined - } - - // TODO: sdjwt - if (offerType === OfferedCredentialType.CredentialSupported) { - const scope = - 'scope' in credentialWithMetadata.credentialSupported - ? credentialWithMetadata.credentialSupported.scope - : undefined - if (format === OpenIdCredentialFormatProfile.SdJwtVc && !scope) { - throw new AriesFrameworkError('Scope is required the request the issuance of a SdJwtVc') - } - - return scope as string - } - - return undefined - } - private getAuthDetailsFromOfferedCredential( credentialWithMetadata: OfferedCredentialWithMetadata, authDetailsLocation: string | undefined, @@ -361,10 +335,6 @@ export class OpenId4VciHolderService { .map((credential) => this.getAuthDetailsFromOfferedCredential(credential, authDetailsLocation, version)) .filter((authDetail): authDetail is AuthorizationDetails => authDetail !== undefined) - const scopes = offeredCredentialsWithMetadata - .map((credential) => this.getScopeForOfferedCredential(credential, version)) - .filter((scope): scope is string => scope !== undefined) - const { clientId, redirectUri, scope } = authCodeFlowOptions const authorizationRequestUri = await createAuthorizationRequestUri({ clientId, @@ -372,9 +342,9 @@ export class OpenId4VciHolderService { redirectUri, credentialOffer: credentialOfferPayload, codeChallengeMethod: CodeChallengeMethod.SHA256, - // TODO: sdjwt don't pass scope, it is always obtained from the metadata now - scope: [...(scope ?? []), ...scopes], + // TODO: Read HAIP SdJwtVc's should always be requested via scopes // TODO: should we now always use scopes instead of authDetails? or both???? + scope: [...(scope ?? [])], authDetails, metadata, }) @@ -723,10 +693,6 @@ export class OpenId4VciHolderService { const format = getUniformFormat(credentialResponse.successBody.format) if (format === OpenIdCredentialFormatProfile.SdJwtVc) { - const sdJwtVcService = agentContext.dependencyManager.resolve(SdJwtVcService) - if (!sdJwtVcService) - throw new AriesFrameworkError('Received an SdJwtVc but no SdJwtVc-Module available for the agent.') - if (typeof credentialResponse.successBody.credential !== 'string') throw new AriesFrameworkError( `Received a credential of format ${ @@ -734,7 +700,9 @@ export class OpenId4VciHolderService { }, but the credential is not a string. ${JSON.stringify(credentialResponse.successBody.credential)}` ) - const sdJwtVcRecord = await sdJwtVcService.fromString(agentContext, credentialResponse.successBody.credential, { + const sdJwtVcApi = getApiForModuleByName(agentContext, 'SdJwtVcModule') + if (!sdJwtVcApi) throw new AriesFrameworkError(`Could not find the SdJwtVcApi`) + const sdJwtVcRecord = await sdJwtVcApi.fromSerializedJwt(credentialResponse.successBody.credential, { holderDidUrl, }) diff --git a/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts b/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts index 83d34e8673..6839bae39c 100644 --- a/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts +++ b/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts @@ -36,7 +36,7 @@ import { equalsIgnoreOrder, } from '@aries-framework/core' import { agentDependencies } from '@aries-framework/node' -import { SdJwtVcModule, SdJwtVcService } from '@aries-framework/sd-jwt-vc' +import { SdJwtVcApi, SdJwtVcModule } from '@aries-framework/sd-jwt-vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { cleanAll, enableNetConnect } from 'nock' @@ -241,10 +241,8 @@ describe('OpenId4VcIssuer', () => { credentialSupported: CredentialSupportedWithId ) { if (credentialSupported.format === 'vc+sd-jwt' && typeof sphereonVerifiableCredential === 'string') { - const sdJwtVcService = holder.context.dependencyManager.resolve(SdJwtVcService) - - // this throws if invalid - await sdJwtVcService.fromString(agentContext, sphereonVerifiableCredential, { holderDidUrl: holderKid }) + const api = agentContext.dependencyManager.resolve(SdJwtVcApi) + await api.fromSerializedJwt(sphereonVerifiableCredential, { holderDidUrl: holderKid }) return } diff --git a/packages/openid4vc-verifier/package.json b/packages/openid4vc-verifier/package.json index 2b5eff2612..b01b4b0e3d 100644 --- a/packages/openid4vc-verifier/package.json +++ b/packages/openid4vc-verifier/package.json @@ -26,7 +26,6 @@ "dependencies": { "@aries-framework/askar": "^0.4.2", "@aries-framework/core": "0.4.2", - "@aries-framework/sd-jwt-vc": "^0.4.2", "@sphereon/did-auth-siop": "^0.5.0-unstable.7", "@sphereon/pex": "2.2.0", "@sphereon/pex-models": "^2.1.1", @@ -36,6 +35,7 @@ }, "devDependencies": { "@aries-framework/node": "^0.4.2", + "@aries-framework/sd-jwt-vc": "^0.4.2", "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", "@types/body-parser": "^1.19.5", "@types/express": "^4.17.21", diff --git a/packages/sd-jwt-vc/package.json b/packages/sd-jwt-vc/package.json index df62927318..62f2925c63 100644 --- a/packages/sd-jwt-vc/package.json +++ b/packages/sd-jwt-vc/package.json @@ -31,6 +31,7 @@ "jwt-sd": "^0.1.2" }, "devDependencies": { + "@aries-framework/node": "^0.4.2", "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", "reflect-metadata": "^0.1.13", "rimraf": "^4.4.0", diff --git a/packages/sd-jwt-vc/src/SdJwtVcApi.ts b/packages/sd-jwt-vc/src/SdJwtVcApi.ts index 2c809ba9eb..cdd0eb49a0 100644 --- a/packages/sd-jwt-vc/src/SdJwtVcApi.ts +++ b/packages/sd-jwt-vc/src/SdJwtVcApi.ts @@ -1,7 +1,7 @@ import type { SdJwtVcCreateOptions, + SdJwtVcFromSerializedJwtOptions, SdJwtVcPresentOptions, - SdJwtVcReceiveOptions, SdJwtVcVerifyOptions, } from './SdJwtVcOptions' import type { SdJwtVcVerificationResult } from './SdJwtVcService' @@ -34,15 +34,19 @@ export class SdJwtVcApi { /** * - * Validates and stores an sd-jwt-vc from the perspective of an holder - * + * Get and validate a sd-jwt-vc from a serialized JWT. */ - public async storeCredential(sdJwtVcCompact: string, options: SdJwtVcReceiveOptions): Promise { - return await this.sdJwtVcService.storeCredential(this.agentContext, sdJwtVcCompact, options) + public async fromSerializedJwt(sdJwtVcCompact: string, options: SdJwtVcFromSerializedJwtOptions) { + return await this.sdJwtVcService.fromSerializedJwt(this.agentContext, sdJwtVcCompact, options) } - public async storeCredential2(sdJwtVcRecord: SdJwtVcRecord): Promise { - return await this.sdJwtVcService.storeCredential2(this.agentContext, sdJwtVcRecord) + /** + * + * Stores and sd-jwt-vc record + * + */ + public async storeCredential(sdJwtVcRecord: SdJwtVcRecord): Promise { + return await this.sdJwtVcService.storeCredential(this.agentContext, sdJwtVcRecord) } /** diff --git a/packages/sd-jwt-vc/src/SdJwtVcOptions.ts b/packages/sd-jwt-vc/src/SdJwtVcOptions.ts index f247b73215..7f94fc6bab 100644 --- a/packages/sd-jwt-vc/src/SdJwtVcOptions.ts +++ b/packages/sd-jwt-vc/src/SdJwtVcOptions.ts @@ -9,16 +9,11 @@ export type SdJwtVcCreateOptions = Recor hashingAlgorithm?: HashName } -export type SdJwtVcFromStringOptions = { +export type SdJwtVcFromSerializedJwtOptions = { issuerDidUrl?: string holderDidUrl: string } -export type SdJwtVcReceiveOptions = { - issuerDidUrl: string - holderDidUrl: string -} - /** * `includedDisclosureIndices` is not the best API, but it is the best alternative until something like `PEX` is supported */ diff --git a/packages/sd-jwt-vc/src/SdJwtVcService.ts b/packages/sd-jwt-vc/src/SdJwtVcService.ts index bec1ebdad6..561087c87d 100644 --- a/packages/sd-jwt-vc/src/SdJwtVcService.ts +++ b/packages/sd-jwt-vc/src/SdJwtVcService.ts @@ -1,8 +1,7 @@ import type { SdJwtVcCreateOptions, SdJwtVcPresentOptions, - SdJwtVcReceiveOptions, - SdJwtVcFromStringOptions, + SdJwtVcFromSerializedJwtOptions, SdJwtVcVerifyOptions, } from './SdJwtVcOptions' import type { AgentContext, JwkJson, Query } from '@aries-framework/core' @@ -170,21 +169,19 @@ export class SdJwtVcService { }, }) - await this.sdJwtVcRepository.save(agentContext, sdJwtVcRecord) - return { sdJwtVcRecord, compact, } } - public async fromString< + public async fromSerializedJwt< Header extends Record = Record, Payload extends Record = Record >( agentContext: AgentContext, sdJwtVcCompact: string, - { issuerDidUrl, holderDidUrl }: SdJwtVcFromStringOptions + { issuerDidUrl, holderDidUrl }: SdJwtVcFromSerializedJwtOptions ): Promise { const sdJwtVc = SdJwtVc.fromCompact(sdJwtVcCompact) @@ -237,50 +234,7 @@ export class SdJwtVcService { return sdJwtVcRecord } - public async storeCredential2(agentContext: AgentContext, sdJwtVcRecord: SdJwtVcRecord): Promise { - await this.sdJwtVcRepository.save(agentContext, sdJwtVcRecord) - return sdJwtVcRecord - } - - public async storeCredential< - Header extends Record = Record, - Payload extends Record = Record - >( - agentContext: AgentContext, - sdJwtVcCompact: string, - { issuerDidUrl, holderDidUrl }: SdJwtVcReceiveOptions - ): Promise { - const sdJwtVc = SdJwtVc.fromCompact(sdJwtVcCompact) - - if (!sdJwtVc.signature) { - throw new SdJwtVcError('A signature must be included for an sd-jwt-vc') - } - - const { verificationMethod: issuerVerificationMethod } = await this.resolveDidUrl(agentContext, issuerDidUrl) - const issuerKey = getKeyFromVerificationMethod(issuerVerificationMethod) - - const { isSignatureValid } = await sdJwtVc.verify(this.verifier(agentContext, issuerKey)) - - if (!isSignatureValid) { - throw new SdJwtVcError('sd-jwt-vc has an invalid signature from the issuer') - } - - const { verificationMethod: holderVerificiationMethod } = await this.resolveDidUrl(agentContext, holderDidUrl) - const holderKey = getKeyFromVerificationMethod(holderVerificiationMethod) - const holderKeyJwk = getJwkFromKey(holderKey).toJson() - - sdJwtVc.assertClaimInPayload('cnf', { jwk: holderKeyJwk }) - - const sdJwtVcRecord = new SdJwtVcRecord({ - sdJwtVc: { - header: sdJwtVc.header, - payload: sdJwtVc.payload, - signature: sdJwtVc.signature, - disclosures: sdJwtVc.disclosures?.map((d) => d.decoded), - holderDidUrl, - }, - }) - + public async storeCredential(agentContext: AgentContext, sdJwtVcRecord: SdJwtVcRecord): Promise { await this.sdJwtVcRepository.save(agentContext, sdJwtVcRecord) return sdJwtVcRecord diff --git a/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts b/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts index a0347e3c46..b31faadbe8 100644 --- a/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts +++ b/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts @@ -12,9 +12,9 @@ import { Agent, TypedArrayEncoder, } from '@aries-framework/core' +import { agentDependencies } from '@aries-framework/node' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' -import { agentDependencies } from '../../../core/tests' import { SdJwtVcService } from '../SdJwtVcService' import { SdJwtVcRepository } from '../repository' @@ -251,11 +251,13 @@ describe('SdJwtVcService', () => { test('Receive sd-jwt-vc from a basic payload without disclosures', async () => { const sdJwtVc = simpleJwtVc - const sdJwtVcRecord = await sdJwtVcService.storeCredential(agent.context, sdJwtVc, { + const sdJwtVcRecord = await SdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { issuerDidUrl, holderDidUrl, }) + await sdJwtVcService.storeCredential(agent.context, sdJwtVcRecord) + expect(sdJwtVcRecord.sdJwtVc.header).toEqual({ alg: 'EdDSA', typ: 'vc+sd-jwt', @@ -276,11 +278,13 @@ describe('SdJwtVcService', () => { test('Receive sd-jwt-vc from a basic payload with a disclosure', async () => { const sdJwtVc = sdJwtVcWithSingleDisclosure - const sdJwtVcRecord = await sdJwtVcService.storeCredential(agent.context, sdJwtVc, { + const sdJwtVcRecord = await SdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { issuerDidUrl, holderDidUrl, }) + await sdJwtVcService.storeCredential(agent.context, sdJwtVcRecord) + expect(sdJwtVcRecord.sdJwtVc.header).toEqual({ alg: 'EdDSA', typ: 'vc+sd-jwt', @@ -308,7 +312,12 @@ describe('SdJwtVcService', () => { test('Receive sd-jwt-vc from a basic payload with multiple (nested) disclosure', async () => { const sdJwtVc = complexSdJwtVc - const sdJwtVcRecord = await sdJwtVcService.storeCredential(agent.context, sdJwtVc, { holderDidUrl, issuerDidUrl }) + const sdJwtVcRecord = await SdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { + issuerDidUrl, + holderDidUrl, + }) + + await sdJwtVcService.storeCredential(agent.context, sdJwtVcRecord) expect(sdJwtVcRecord.sdJwtVc.header).toEqual({ alg: 'EdDSA', @@ -373,7 +382,12 @@ describe('SdJwtVcService', () => { test('Present sd-jwt-vc from a basic payload without disclosures', async () => { const sdJwtVc = simpleJwtVc - const sdJwtVcRecord = await sdJwtVcService.storeCredential(agent.context, sdJwtVc, { issuerDidUrl, holderDidUrl }) + const sdJwtVcRecord = await SdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { + issuerDidUrl, + holderDidUrl, + }) + + await sdJwtVcService.storeCredential(agent.context, sdJwtVcRecord) const presentation = await sdJwtVcService.present(agent.context, sdJwtVcRecord, { verifierMetadata: { @@ -389,7 +403,12 @@ describe('SdJwtVcService', () => { test('Present sd-jwt-vc from a basic payload with a disclosure', async () => { const sdJwtVc = sdJwtVcWithSingleDisclosure - const sdJwtVcRecord = await sdJwtVcService.storeCredential(agent.context, sdJwtVc, { holderDidUrl, issuerDidUrl }) + const sdJwtVcRecord = await SdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { + issuerDidUrl, + holderDidUrl, + }) + + await sdJwtVcService.storeCredential(agent.context, sdJwtVcRecord) const presentation = await sdJwtVcService.present(agent.context, sdJwtVcRecord, { verifierMetadata: { @@ -406,7 +425,12 @@ describe('SdJwtVcService', () => { test('Present sd-jwt-vc from a basic payload with multiple (nested) disclosure', async () => { const sdJwtVc = complexSdJwtVc - const sdJwtVcRecord = await sdJwtVcService.storeCredential(agent.context, sdJwtVc, { holderDidUrl, issuerDidUrl }) + const sdJwtVcRecord = await SdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { + issuerDidUrl, + holderDidUrl, + }) + + await sdJwtVcService.storeCredential(agent.context, sdJwtVcRecord) const presentation = await sdJwtVcService.present(agent.context, sdJwtVcRecord, { verifierMetadata: { @@ -425,7 +449,12 @@ describe('SdJwtVcService', () => { test('Verify sd-jwt-vc without disclosures', async () => { const sdJwtVc = simpleJwtVc - const sdJwtVcRecord = await sdJwtVcService.storeCredential(agent.context, sdJwtVc, { holderDidUrl, issuerDidUrl }) + const sdJwtVcRecord = await SdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { + issuerDidUrl, + holderDidUrl, + }) + + await sdJwtVcService.storeCredential(agent.context, sdJwtVcRecord) const presentation = await sdJwtVcService.present(agent.context, sdJwtVcRecord, { verifierMetadata: { @@ -453,7 +482,12 @@ describe('SdJwtVcService', () => { test('Verify sd-jwt-vc with a disclosure', async () => { const sdJwtVc = sdJwtVcWithSingleDisclosure - const sdJwtVcRecord = await sdJwtVcService.storeCredential(agent.context, sdJwtVc, { holderDidUrl, issuerDidUrl }) + const sdJwtVcRecord = await SdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { + issuerDidUrl, + holderDidUrl, + }) + + await sdJwtVcService.storeCredential(agent.context, sdJwtVcRecord) const presentation = await sdJwtVcService.present(agent.context, sdJwtVcRecord, { verifierMetadata: { @@ -482,7 +516,12 @@ describe('SdJwtVcService', () => { test('Verify sd-jwt-vc with multiple (nested) disclosure', async () => { const sdJwtVc = complexSdJwtVc - const sdJwtVcRecord = await sdJwtVcService.storeCredential(agent.context, sdJwtVc, { holderDidUrl, issuerDidUrl }) + const sdJwtVcRecord = await SdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { + issuerDidUrl, + holderDidUrl, + }) + + await sdJwtVcService.storeCredential(agent.context, sdJwtVcRecord) const presentation = await sdJwtVcService.present(agent.context, sdJwtVcRecord, { verifierMetadata: { diff --git a/packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts b/packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts index 9db27d97fe..558cfe5acf 100644 --- a/packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts +++ b/packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts @@ -11,10 +11,10 @@ import { TypedArrayEncoder, utils, } from '@aries-framework/core' +import { agentDependencies } from '@aries-framework/node' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' -import { agentDependencies } from '../../core/tests' -import { SdJwtVcModule } from '../src' +import { SdJwtVcModule, SdJwtVcService } from '../src' const getAgent = (label: string) => new Agent({ @@ -104,7 +104,12 @@ describe('sd-jwt-vc end to end test', () => { }, }) - const sdJwtVcRecord = await holder.modules.sdJwt.storeCredential(compact, { issuerDidUrl, holderDidUrl }) + const sdJwtVcRecord = await SdJwtVcService.fromSerializedJwt(issuer.context, compact, { + issuerDidUrl, + holderDidUrl, + }) + + await holder.modules.sdJwt.storeCredential(sdJwtVcRecord) // Metadata created by the verifier and send out of band by the verifier to the holder const verifierMetadata = { diff --git a/yarn.lock b/yarn.lock index 21b8261a48..df639f93c7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2602,7 +2602,7 @@ "@sphereon/oid4vci-client@file:../Sphereon/sphereon-oidvci-client-0.8.1": version "0.8.1" dependencies: - "@sphereon/oid4vci-common" "file:../../../Library/Caches/Yarn/v6/npm-@sphereon-oid4vci-client-0.8.1-301170cf-5685-4f6d-bb5d-6b488d8f7353-1701331237986/node_modules/@sphereon/sphereon-oid4vci-common-0.8.1" + "@sphereon/oid4vci-common" "file:../../../Library/Caches/Yarn/v6/npm-@sphereon-oid4vci-client-0.8.1-f811adc0-1b57-408f-94f9-ae255119e9b1-1701698647517/node_modules/@sphereon/sphereon-oid4vci-common-0.8.1" "@sphereon/ssi-types" "0.17.2" cross-fetch "^3.1.8" debug "^4.3.4" @@ -2614,6 +2614,21 @@ cross-fetch "^3.1.8" jwt-decode "^3.1.2" +"@sphereon/oid4vci-issuer-server@file:../Sphereon/sphereon-oid4vci-issuer-server-0.8.1": + version "0.8.1" + dependencies: + "@sphereon/oid4vci-common" "file:../../../Library/Caches/Yarn/v6/npm-@sphereon-oid4vci-issuer-server-0.8.1-e621511a-80af-4ac9-9a46-a4bb6eb08dee-1701698647506/node_modules/@sphereon/sphereon-oid4vci-common-0.8.1" + "@sphereon/oid4vci-issuer" "file:../../../Library/Caches/Yarn/v6/npm-@sphereon-oid4vci-issuer-server-0.8.1-e621511a-80af-4ac9-9a46-a4bb6eb08dee-1701698647506/node_modules/@sphereon/sphereon-oid4vci-issuer-0.8.1" + "@sphereon/ssi-express-support" "0.17.2" + "@sphereon/ssi-types" "0.17.2" + body-parser "^1.20.2" + cookie-parser "^1.4.6" + cors "^2.8.5" + dotenv-flow "^3.2.0" + express "^4.18.2" + http-terminator "^3.2.0" + uuid "^9.0.0" + "@sphereon/oid4vci-issuer@file:../Sphereon/sphereon-oid4vci-issuer-0.8.1": version "0.8.1" dependencies: @@ -2654,6 +2669,25 @@ nanoid "^3.3.6" string.prototype.matchall "^4.0.8" +"@sphereon/ssi-express-support@0.17.2": + version "0.17.2" + resolved "https://registry.yarnpkg.com/@sphereon/ssi-express-support/-/ssi-express-support-0.17.2.tgz#bc5c31024a16ee9c7ede99a24893881096d98441" + integrity sha512-OrLC7YAelpUmCIzPRgHM97HBNFqDoSdJNNssstS6Ho0ZXswq4fsPDm+h49+//ogp1ERbuOl9Ywqhp+3DdLZCPA== + dependencies: + body-parser "^1.20.2" + casbin "^5.26.1" + cookie-session "^2.0.0" + cors "^2.8.5" + dotenv-flow "^3.2.0" + express "^4.18.2" + express-session "^1.17.3" + http-terminator "^3.2.0" + morgan "^1.10.0" + openid-client "^5.4.3" + passport "^0.6.0" + qs "^6.11.2" + uint8arrays "^3.1.1" + "@sphereon/ssi-types@0.17.2": version "0.17.2" resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.17.2.tgz#d6b5e9eef1e68d8e9c846c8edf9279257e8e348d" @@ -3775,6 +3809,11 @@ available-typed-arrays@^1.0.5: resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== +await-lock@^2.0.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/await-lock/-/await-lock-2.2.2.tgz#a95a9b269bfd2f69d22b17a321686f551152bcef" + integrity sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw== + axios@^0.21.2: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" @@ -3967,6 +4006,13 @@ base@^0.11.1: mixin-deep "^1.2.0" pascalcase "^0.1.1" +basic-auth@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + bech32@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" @@ -4064,6 +4110,11 @@ body-parser@^1.20.2: type-is "~1.6.18" unpipe "1.0.0" +boolean@^3.1.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/boolean/-/boolean-3.2.0.tgz#9e5294af4e98314494cbb17979fa54ca159f116b" + integrity sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw== + borc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/borc/-/borc-3.0.0.tgz#49ada1be84de86f57bb1bb89789f34c186dfa4fe" @@ -4354,6 +4405,17 @@ canonicalize@^2.0.0: resolved "https://registry.yarnpkg.com/canonicalize/-/canonicalize-2.0.0.tgz#32be2cef4446d67fd5348027a384cae28f17226a" integrity sha512-ulDEYPv7asdKvqahuAY35c1selLdzDwHqugK92hfkzvlDCwXRRelDkR+Er33md/PtnpqHemgkuDPanZ4fiYZ8w== +casbin@^5.26.1: + version "5.28.0" + resolved "https://registry.yarnpkg.com/casbin/-/casbin-5.28.0.tgz#36a961999afcbb50d36973a0f238ef595d804c7a" + integrity sha512-7R1zGDOWUKVowPTT/qTZjm5L5G0ZASQ6dmKIGHYM8KqmkTc28P/KUO9WeaGjLKELnpOCkPIz0EJHw1CaTtgucw== + dependencies: + await-lock "^2.0.1" + buffer "^6.0.3" + csv-parse "^5.3.5" + expression-eval "^5.0.0" + minimatch "^7.4.2" + chalk@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" @@ -4861,16 +4923,52 @@ convert-source-map@^2.0.0: resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== +cookie-parser@^1.4.6: + version "1.4.6" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.6.tgz#3ac3a7d35a7a03bbc7e365073a26074824214594" + integrity sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA== + dependencies: + cookie "0.4.1" + cookie-signature "1.0.6" + +cookie-session@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/cookie-session/-/cookie-session-2.0.0.tgz#d07aa27822f43619e4342df1342268c849833089" + integrity sha512-hKvgoThbw00zQOleSlUr2qpvuNweoqBtxrmx0UFosx6AGi9lYtLoA+RbsvknrEX8Pr6MDbdWAb2j6SnMn+lPsg== + dependencies: + cookies "0.8.0" + debug "3.2.7" + on-headers "~1.0.2" + safe-buffer "5.2.1" + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== +cookie@0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" + integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== + +cookie@0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + cookie@0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +cookies@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.8.0.tgz#1293ce4b391740a8406e3c9870e828c4b54f3f90" + integrity sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow== + dependencies: + depd "~2.0.0" + keygrip "~1.1.0" + copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" @@ -5007,6 +5105,11 @@ cssesc@^3.0.0: resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== +csv-parse@^5.3.5: + version "5.5.2" + resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-5.5.2.tgz#ab525e642093dccff7c5cca5c7b71fd3e99fe8f2" + integrity sha512-YRVtvdtUNXZCMyK5zd5Wty1W6dNTpGKdqQd4EQ8tl/c6KW1aMBB1Kg1ppky5FONKmEqGJ/8WjLlTNLPne4ioVA== + d@1, d@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" @@ -5042,6 +5145,13 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3: dependencies: ms "2.0.0" +debug@3.2.7, debug@^3.1.0, debug@^3.2.6, debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" @@ -5049,13 +5159,6 @@ debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: dependencies: ms "2.1.2" -debug@^3.1.0, debug@^3.2.6, debug@^3.2.7: - version "3.2.7" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" - integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== - dependencies: - ms "^2.1.1" - decamelize-keys@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.1.tgz#04a2d523b2f18d80d0158a43b895d56dff8d19d8" @@ -5169,6 +5272,11 @@ del@^6.0.0: rimraf "^3.0.2" slash "^3.0.0" +delay@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/delay/-/delay-5.0.0.tgz#137045ef1b96e5071060dd5be60bf9334436bd1d" + integrity sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw== + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -5184,7 +5292,7 @@ denodeify@^1.2.1: resolved "https://registry.yarnpkg.com/denodeify/-/denodeify-1.2.1.tgz#3a36287f5034e699e7577901052c2e6c94251631" integrity sha512-KNTihKNmQENUZeKu5fzfpzRqR5S2VMp4gl9RFHiWzj9DfvYQPMJ6XHKNaQxaGCXwPk6y9yme3aUoaiAe+KX+vg== -depd@2.0.0, depd@^2.0.0: +depd@2.0.0, depd@^2.0.0, depd@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== @@ -5301,6 +5409,18 @@ dot-prop@^5.1.0: dependencies: is-obj "^2.0.0" +dotenv-flow@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/dotenv-flow/-/dotenv-flow-3.3.0.tgz#e22341c035d5672b923bf76feb35999f728ee456" + integrity sha512-GLSvRqDZ1TGhloS6ZCZ5chdqqv/3XMqZxAnX9rliJiHn6uyJLguKeu+3M2kcagBkoVCnLWYfbR4rfFe1xSU39A== + dependencies: + dotenv "^8.6.0" + +dotenv@^8.6.0: + version "8.6.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" + integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g== + dotenv@~10.0.0: version "10.0.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81" @@ -5901,6 +6021,20 @@ expo-random@*: dependencies: base64-js "^1.3.0" +express-session@^1.17.3: + version "1.17.3" + resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.17.3.tgz#14b997a15ed43e5949cb1d073725675dd2777f36" + integrity sha512-4+otWXlShYlG1Ma+2Jnn+xgKUZTMJ5QD3YvfilX3AcocOAbIkVylSWEklzALe/+Pu4qV6TYBj5GwOBFfdKqLBw== + dependencies: + cookie "0.4.2" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~2.0.0" + on-headers "~1.0.2" + parseurl "~1.3.3" + safe-buffer "5.2.1" + uid-safe "~2.1.5" + express@^4.17.1, express@^4.18.1, express@^4.18.2: version "4.18.2" resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59" @@ -5938,6 +6072,13 @@ express@^4.17.1, express@^4.18.1, express@^4.18.2: utils-merge "1.0.1" vary "~1.1.2" +expression-eval@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/expression-eval/-/expression-eval-5.0.1.tgz#845758fa9ba64d9edc7b6804ae404934a6cfee6b" + integrity sha512-7SL4miKp19lI834/F6y156xlNg+i9Q41tteuGNCq9C06S78f1bm3BXuvf0+QpQxv369Pv/P2R7Hb17hzxLpbDA== + dependencies: + jsep "^0.3.0" + ext@^1.1.2: version "1.7.0" resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" @@ -6030,6 +6171,13 @@ fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== +fast-printf@^1.6.9: + version "1.6.9" + resolved "https://registry.yarnpkg.com/fast-printf/-/fast-printf-1.6.9.tgz#212f56570d2dc8ccdd057ee93d50dd414d07d676" + integrity sha512-FChq8hbz65WMj4rstcQsFB0O7Cy++nmbNfLYnD9cYv2cRn8EG6k/MGn9kO/tjO66t09DLDugj3yL+V2o6Qftrg== + dependencies: + boolean "^3.1.4" + fast-text-encoding@^1.0.3: version "1.0.6" resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.6.tgz#0aa25f7f638222e3396d72bf936afcf1d42d6867" @@ -6914,6 +7062,16 @@ http-proxy-agent@^5.0.0: agent-base "6" debug "4" +http-terminator@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/http-terminator/-/http-terminator-3.2.0.tgz#bc158d2694b733ca4fbf22a35065a81a609fb3e9" + integrity sha512-JLjck1EzPaWjsmIf8bziM3p9fgR1Y3JoUKAkyYEbZmFrIvJM6I8vVJfBGWlEtV9IWOvzNnaTtjuwZeBY2kwB4g== + dependencies: + delay "^5.0.0" + p-wait-for "^3.2.0" + roarr "^7.0.4" + type-fest "^2.3.3" + https-proxy-agent@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" @@ -8104,6 +8262,11 @@ joi@^17.2.1: "@sideway/formula" "^3.0.1" "@sideway/pinpoint" "^2.0.0" +jose@^4.15.1: + version "4.15.4" + resolved "https://registry.yarnpkg.com/jose/-/jose-4.15.4.tgz#02a9a763803e3872cf55f29ecef0dfdcc218cc03" + integrity sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ== + js-sdsl@^4.1.4: version "4.4.0" resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.0.tgz#8b437dbe642daa95760400b602378ed8ffea8430" @@ -8164,6 +8327,11 @@ jscodeshift@^0.13.1: temp "^0.8.4" write-file-atomic "^2.3.0" +jsep@^0.3.0: + version "0.3.5" + resolved "https://registry.yarnpkg.com/jsep/-/jsep-0.3.5.tgz#3fd79ebd92f6f434e4857d5272aaeef7d948264d" + integrity sha512-AoRLBDc6JNnKjNcmonituEABS5bcfqDhQAWWXNTFrqu6nVXBpBAGfcoTGZMFlIrh9FjmE1CQyX9CTNwZrXMMDA== + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -8290,6 +8458,13 @@ jwt-sd@^0.1.2: dependencies: buffer "^6.0.3" +keygrip@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.1.0.tgz#871b1681d5e159c62a445b0c74b615e0917e7226" + integrity sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ== + dependencies: + tsscmp "1.0.6" + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -9371,6 +9546,17 @@ modify-values@^1.0.0: resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== +morgan@^1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/morgan/-/morgan-1.10.0.tgz#091778abc1fc47cd3509824653dae1faab6b17d7" + integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== + dependencies: + basic-auth "~2.0.1" + debug "2.6.9" + depd "~2.0.0" + on-finished "~2.3.0" + on-headers "~1.0.2" + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -9391,11 +9577,6 @@ msrcrypto@^1.5.6: resolved "https://registry.yarnpkg.com/msrcrypto/-/msrcrypto-1.5.8.tgz#be419be4945bf134d8af52e9d43be7fa261f4a1c" integrity sha512-ujZ0TRuozHKKm6eGbKHfXef7f+esIhEckmThVnz7RNyiOJd7a6MXj2JGBoL9cnPDW+JMG16MoTUh5X+XXjI66Q== -multiformats@^11.0.2: - version "11.0.2" - resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-11.0.2.tgz#b14735efc42cd8581e73895e66bebb9752151b60" - integrity sha512-b5mYMkOkARIuVZCpvijFj9a6m5wMVLC7cf/jIPd5D/ARDOfLC5+IFkbgDXQgcU2goIsTD/O9NY4DI/Mt4OGvlg== - multer@^1.4.5-lts.1: version "1.4.5-lts.1" resolved "https://registry.yarnpkg.com/multer/-/multer-1.4.5-lts.1.tgz#803e24ad1984f58edffbc79f56e305aec5cfd1ac" @@ -9409,6 +9590,11 @@ multer@^1.4.5-lts.1: type-is "^1.6.4" xtend "^4.0.0" +multiformats@^11.0.2: + version "11.0.2" + resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-11.0.2.tgz#b14735efc42cd8581e73895e66bebb9752151b60" + integrity sha512-b5mYMkOkARIuVZCpvijFj9a6m5wMVLC7cf/jIPd5D/ARDOfLC5+IFkbgDXQgcU2goIsTD/O9NY4DI/Mt4OGvlg== + multiformats@^9.4.2, multiformats@^9.6.5, multiformats@^9.9.0: version "9.9.0" resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37" @@ -10049,6 +10235,11 @@ object-copy@^0.1.0: define-property "^0.2.5" kind-of "^3.0.3" +object-hash@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" + integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== + object-inspect@^1.10.3, object-inspect@^1.12.3, object-inspect@^1.9.0: version "1.12.3" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9" @@ -10092,6 +10283,11 @@ object.values@^1.1.6: define-properties "^1.1.4" es-abstract "^1.20.4" +oidc-token-hash@^5.0.3: + version "5.0.3" + resolved "https://registry.yarnpkg.com/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz#9a229f0a1ce9d4fc89bcaee5478c97a889e7b7b6" + integrity sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw== + on-finished@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" @@ -10141,6 +10337,16 @@ open@^8.4.0: is-docker "^2.1.1" is-wsl "^2.2.0" +openid-client@^5.4.3: + version "5.6.1" + resolved "https://registry.yarnpkg.com/openid-client/-/openid-client-5.6.1.tgz#8f7526a50c290a5e28a7fe21b3ece3107511bc73" + integrity sha512-PtrWsY+dXg6y8mtMPyL/namZSYVz8pjXz3yJiBNZsEdCnu9miHLB4ELVC85WvneMKo2Rg62Ay7NkuCpM0bgiLQ== + dependencies: + jose "^4.15.1" + lru-cache "^6.0.0" + object-hash "^2.2.0" + oidc-token-hash "^5.0.3" + optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -10282,7 +10488,7 @@ p-reduce@2.1.0, p-reduce@^2.0.0, p-reduce@^2.1.0: resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-2.1.0.tgz#09408da49507c6c274faa31f28df334bc712b64a" integrity sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw== -p-timeout@^3.2.0: +p-timeout@^3.0.0, p-timeout@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== @@ -10299,6 +10505,13 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +p-wait-for@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/p-wait-for/-/p-wait-for-3.2.0.tgz#640429bcabf3b0dd9f492c31539c5718cb6a3f1f" + integrity sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA== + dependencies: + p-timeout "^3.0.0" + p-waterfall@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/p-waterfall/-/p-waterfall-2.1.1.tgz#63153a774f472ccdc4eb281cdb2967fcf158b2ee" @@ -10415,6 +10628,20 @@ pascalcase@^0.1.1: resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" integrity sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw== +passport-strategy@1.x.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" + integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== + +passport@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/passport/-/passport-0.6.0.tgz#e869579fab465b5c0b291e841e6cc95c005fac9d" + integrity sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug== + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + utils-merge "^1.0.1" + path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" @@ -10470,6 +10697,11 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== +pause@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/pause/-/pause-0.0.1.tgz#1d408b3fdb76923b9543d96fb4c9dfd535d9cb5d" + integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg== + picocolors@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" @@ -10799,6 +11031,11 @@ quick-lru@^4.0.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== +random-bytes@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" + integrity sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ== + range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -11320,6 +11557,15 @@ rimraf@~2.6.2: dependencies: glob "^7.1.3" +roarr@^7.0.4: + version "7.21.0" + resolved "https://registry.yarnpkg.com/roarr/-/roarr-7.21.0.tgz#5c3c7b88802f85219b77c2e6f166532aa31c675d" + integrity sha512-d1rPLcHmQID3GsA3p9d5vKSZYlvrTWhjbmeg9DT5DcPoLpH85VzPmkLkGKhQv376+dfkApaHwNbpYEwDB77Ibg== + dependencies: + fast-printf "^1.6.9" + safe-stable-stringify "^2.4.3" + semver-compare "^1.0.0" + run-async@^2.4.0: version "2.4.1" resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" @@ -11382,6 +11628,11 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" +safe-stable-stringify@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" + integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g== + "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -11399,6 +11650,11 @@ scheduler@^0.23.0: dependencies: loose-envify "^1.1.0" +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== + "semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: version "5.7.2" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" @@ -12382,6 +12638,11 @@ tslog@^4.8.2: resolved "https://registry.yarnpkg.com/tslog/-/tslog-4.8.2.tgz#dbb0c96249e387e8a711ae6e077330ba1ef102c9" integrity sha512-eAKIRjxfSKYLs06r1wT7oou6Uv9VN6NW9g0JPidBlqQwPBBl5+84dm7r8zSOPVq1kyfEw1P6B3/FLSpZCorAgA== +tsscmp@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/tsscmp/-/tsscmp-1.0.6.tgz#85b99583ac3589ec4bfef825b5000aa911d605eb" + integrity sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -12463,6 +12724,11 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^2.3.3: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== + type-fest@^3.2.0: version "3.9.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.9.0.tgz#36a9e46e6583649f9e6098b267bc577275e9e4f4" @@ -12558,6 +12824,13 @@ uglify-js@^3.1.4: resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g== +uid-safe@~2.1.5: + version "2.1.5" + resolved "https://registry.yarnpkg.com/uid-safe/-/uid-safe-2.1.5.tgz#2b3d5c7240e8fc2e58f8aa269e5ee49c0857bd3a" + integrity sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA== + dependencies: + random-bytes "~1.0.0" + uint8arrays@^3.0.0, uint8arrays@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.1.1.tgz#2d8762acce159ccd9936057572dade9459f65ae0" @@ -12740,7 +13013,7 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -utils-merge@1.0.1: +utils-merge@1.0.1, utils-merge@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== From 34b39d85599902782adb577689ba2301f3356770 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Tue, 5 Dec 2023 15:49:20 +0100 Subject: [PATCH 080/115] feat: restructure openid4vc modules --- demo-openid/package.json | 4 +- demo-openid/src/Holder.ts | 4 +- demo-openid/src/HolderInquirer.ts | 3 +- demo-openid/src/Issuer.ts | 8 +- demo-openid/src/Verifier.ts | 10 +- packages/openid4vc-holder/package.json | 42 ----- packages/openid4vc-holder/src/issuance.ts | 19 -- packages/openid4vc-holder/src/presentation.ts | 11 -- packages/openid4vc-issuer/jest.config.ts | 14 -- packages/openid4vc-issuer/package.json | 42 ----- packages/openid4vc-issuer/tsconfig.build.json | 8 - packages/openid4vc-issuer/tsconfig.json | 7 - packages/openid4vc-verifier/jest.config.ts | 14 -- .../openid4vc-verifier/tsconfig.build.json | 8 - packages/openid4vc-verifier/tsconfig.json | 7 - .../{openid4vc-holder => openid4vc}/README.md | 0 .../jest.config.ts | 0 .../package.json | 22 ++- packages/openid4vc/src/index.ts | 3 + .../openid4vc-holder}/OpenId4VcHolderApi.ts | 9 +- .../OpenId4VcHolderModule.ts | 4 +- .../openid4vc/src/openid4vc-holder/README.md | 177 ++++++++++++++++++ .../openid4vc-holder/__tests__}/fixtures.ts | 0 .../__tests__}/fixtures_vp.ts | 0 .../openId4vc-holder-module.test.ts | 8 +- .../__tests__}/openid4vci-holder.e2e.test.ts | 21 +-- .../__tests__}/openid4vp-holder.e2e.test.ts | 6 +- .../src/openid4vc-holder/__tests__}/setup.ts | 0 .../src/openid4vc-holder}/index.ts | 2 +- .../presentation/OpenId4VpHolderService.ts | 6 +- .../OpenId4VpHolderServiceOptions.ts | 0 .../PresentationExchangeService.ts | 0 .../openid4vc-holder}/presentation/index.ts | 0 .../selection/PexCredentialSelection.ts | 0 .../presentation/selection/example.md | 0 .../presentation/selection/index.ts | 0 .../presentation/selection/types.ts | 0 .../presentation/transform.ts | 0 .../reception}/OpenId4VciHolderService.ts | 0 .../OpenId4VciHolderServiceOptions.ts | 0 .../src/openid4vc-holder/reception}/index.ts | 0 .../reception}/utils/Formats.ts | 0 .../reception}/utils/IssuerMetadataUtils.ts | 0 .../__tests__/claimFormatMapping.test.ts | 0 .../reception}/utils/claimFormatMapping.ts | 0 .../reception}/utils/index.ts | 0 .../openid4vc-issuer}/OpenId4VcIssuerApi.ts | 6 +- .../OpenId4VcIssuerModule.ts | 0 .../OpenId4VcIssuerModuleConfig.ts | 0 .../OpenId4VcIssuerService.ts | 11 +- .../OpenId4VcIssuerServiceOptions.ts | 10 +- .../src}/openid4vc-issuer/README.md | 0 .../openId4vc-issuer-module.test.ts | 8 +- .../__tests__}/openid4vc-issuer.e2e.test.ts | 23 +-- .../src/openid4vc-issuer/__tests__}/setup.ts | 0 .../src/openid4vc-issuer}/index.ts | 6 - .../router/OpenId4VcIEndpointConfiguration.ts | 0 .../router/accessTokenEndpoint.ts | 0 .../src/openid4vc-issuer}/router/utils.ts | 0 .../InMemoryVerifierSessionManager.ts | 0 .../OpenId4VcVerifierApi.ts | 4 +- .../OpenId4VcVerifierModule.ts | 0 .../OpenId4VcVerifierModuleConfig.ts | 0 .../OpenId4VcVerifierService.ts | 4 +- .../OpenId4VcVerifierServiceOptions.ts | 2 +- .../src}/openid4vc-verifier/README.md | 0 .../openId4vc-verifier-module.test.ts | 6 +- .../__tests__}/openid4vc-verifier.e2e.test.ts | 4 +- .../openid4vc-verifier/__tests__}/setup.ts | 0 .../src/openid4vc-verifier}/index.ts | 6 - .../src/openid4vc-verifier}/utils.ts | 0 packages/openid4vc/tests/setup.ts | 1 + .../tsconfig.build.json | 0 .../tsconfig.json | 0 .../src/__tests__/SdJwtVcService.test.ts | 18 +- packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts | 4 +- yarn.lock | 16 +- 77 files changed, 286 insertions(+), 302 deletions(-) delete mode 100644 packages/openid4vc-holder/package.json delete mode 100644 packages/openid4vc-holder/src/issuance.ts delete mode 100644 packages/openid4vc-holder/src/presentation.ts delete mode 100644 packages/openid4vc-issuer/jest.config.ts delete mode 100644 packages/openid4vc-issuer/package.json delete mode 100644 packages/openid4vc-issuer/tsconfig.build.json delete mode 100644 packages/openid4vc-issuer/tsconfig.json delete mode 100644 packages/openid4vc-verifier/jest.config.ts delete mode 100644 packages/openid4vc-verifier/tsconfig.build.json delete mode 100644 packages/openid4vc-verifier/tsconfig.json rename packages/{openid4vc-holder => openid4vc}/README.md (100%) rename packages/{openid4vc-holder => openid4vc}/jest.config.ts (100%) rename packages/{openid4vc-verifier => openid4vc}/package.json (64%) create mode 100644 packages/openid4vc/src/index.ts rename packages/{openid4vc-holder/src => openid4vc/src/openid4vc-holder}/OpenId4VcHolderApi.ts (97%) rename packages/{openid4vc-holder/src => openid4vc/src/openid4vc-holder}/OpenId4VcHolderModule.ts (91%) create mode 100644 packages/openid4vc/src/openid4vc-holder/README.md rename packages/{openid4vc-holder/tests => openid4vc/src/openid4vc-holder/__tests__}/fixtures.ts (100%) rename packages/{openid4vc-holder/tests => openid4vc/src/openid4vc-holder/__tests__}/fixtures_vp.ts (100%) rename packages/{openid4vc-holder/tests => openid4vc/src/openid4vc-holder/__tests__}/openId4vc-holder-module.test.ts (78%) rename packages/{openid4vc-holder/tests => openid4vc/src/openid4vc-holder/__tests__}/openid4vci-holder.e2e.test.ts (99%) rename packages/{openid4vc-holder/tests => openid4vc/src/openid4vc-holder/__tests__}/openid4vp-holder.e2e.test.ts (99%) rename packages/{openid4vc-holder/tests => openid4vc/src/openid4vc-holder/__tests__}/setup.ts (100%) rename packages/{openid4vc-holder/src => openid4vc/src/openid4vc-holder}/index.ts (79%) rename packages/{openid4vc-verifier/src => openid4vc/src/openid4vc-holder}/presentation/OpenId4VpHolderService.ts (98%) rename packages/{openid4vc-verifier/src => openid4vc/src/openid4vc-holder}/presentation/OpenId4VpHolderServiceOptions.ts (100%) rename packages/{openid4vc-verifier/src => openid4vc/src/openid4vc-holder}/presentation/PresentationExchangeService.ts (100%) rename packages/{openid4vc-verifier/src => openid4vc/src/openid4vc-holder}/presentation/index.ts (100%) rename packages/{openid4vc-verifier/src => openid4vc/src/openid4vc-holder}/presentation/selection/PexCredentialSelection.ts (100%) rename packages/{openid4vc-verifier/src => openid4vc/src/openid4vc-holder}/presentation/selection/example.md (100%) rename packages/{openid4vc-verifier/src => openid4vc/src/openid4vc-holder}/presentation/selection/index.ts (100%) rename packages/{openid4vc-verifier/src => openid4vc/src/openid4vc-holder}/presentation/selection/types.ts (100%) rename packages/{openid4vc-verifier/src => openid4vc/src/openid4vc-holder}/presentation/transform.ts (100%) rename packages/{openid4vc-issuer/src/issuance => openid4vc/src/openid4vc-holder/reception}/OpenId4VciHolderService.ts (100%) rename packages/{openid4vc-issuer/src/issuance => openid4vc/src/openid4vc-holder/reception}/OpenId4VciHolderServiceOptions.ts (100%) rename packages/{openid4vc-issuer/src/issuance => openid4vc/src/openid4vc-holder/reception}/index.ts (100%) rename packages/{openid4vc-issuer/src/issuance => openid4vc/src/openid4vc-holder/reception}/utils/Formats.ts (100%) rename packages/{openid4vc-issuer/src/issuance => openid4vc/src/openid4vc-holder/reception}/utils/IssuerMetadataUtils.ts (100%) rename packages/{openid4vc-issuer/src/issuance => openid4vc/src/openid4vc-holder/reception}/utils/__tests__/claimFormatMapping.test.ts (100%) rename packages/{openid4vc-issuer/src/issuance => openid4vc/src/openid4vc-holder/reception}/utils/claimFormatMapping.ts (100%) rename packages/{openid4vc-issuer/src/issuance => openid4vc/src/openid4vc-holder/reception}/utils/index.ts (100%) rename packages/{openid4vc-issuer/src => openid4vc/src/openid4vc-issuer}/OpenId4VcIssuerApi.ts (95%) rename packages/{openid4vc-issuer/src => openid4vc/src/openid4vc-issuer}/OpenId4VcIssuerModule.ts (100%) rename packages/{openid4vc-issuer/src => openid4vc/src/openid4vc-issuer}/OpenId4VcIssuerModuleConfig.ts (100%) rename packages/{openid4vc-issuer/src => openid4vc/src/openid4vc-issuer}/OpenId4VcIssuerService.ts (98%) rename packages/{openid4vc-issuer/src => openid4vc/src/openid4vc-issuer}/OpenId4VcIssuerServiceOptions.ts (95%) rename packages/{ => openid4vc/src}/openid4vc-issuer/README.md (100%) rename packages/{openid4vc-issuer/tests => openid4vc/src/openid4vc-issuer/__tests__}/openId4vc-issuer-module.test.ts (83%) rename packages/{openid4vc-issuer/tests => openid4vc/src/openid4vc-issuer/__tests__}/openid4vc-issuer.e2e.test.ts (98%) rename packages/{openid4vc-issuer/tests => openid4vc/src/openid4vc-issuer/__tests__}/setup.ts (100%) rename packages/{openid4vc-issuer/src => openid4vc/src/openid4vc-issuer}/index.ts (59%) rename packages/{openid4vc-issuer/src => openid4vc/src/openid4vc-issuer}/router/OpenId4VcIEndpointConfiguration.ts (100%) rename packages/{openid4vc-issuer/src => openid4vc/src/openid4vc-issuer}/router/accessTokenEndpoint.ts (100%) rename packages/{openid4vc-issuer/src => openid4vc/src/openid4vc-issuer}/router/utils.ts (100%) rename packages/{openid4vc-verifier/src => openid4vc/src/openid4vc-verifier}/InMemoryVerifierSessionManager.ts (100%) rename packages/{openid4vc-verifier/src => openid4vc/src/openid4vc-verifier}/OpenId4VcVerifierApi.ts (94%) rename packages/{openid4vc-verifier/src => openid4vc/src/openid4vc-verifier}/OpenId4VcVerifierModule.ts (100%) rename packages/{openid4vc-verifier/src => openid4vc/src/openid4vc-verifier}/OpenId4VcVerifierModuleConfig.ts (100%) rename packages/{openid4vc-verifier/src => openid4vc/src/openid4vc-verifier}/OpenId4VcVerifierService.ts (99%) rename packages/{openid4vc-verifier/src => openid4vc/src/openid4vc-verifier}/OpenId4VcVerifierServiceOptions.ts (98%) rename packages/{ => openid4vc/src}/openid4vc-verifier/README.md (100%) rename packages/{openid4vc-verifier/tests => openid4vc/src/openid4vc-verifier/__tests__}/openId4vc-verifier-module.test.ts (82%) rename packages/{openid4vc-verifier/tests => openid4vc/src/openid4vc-verifier/__tests__}/openid4vc-verifier.e2e.test.ts (99%) rename packages/{openid4vc-verifier/tests => openid4vc/src/openid4vc-verifier/__tests__}/setup.ts (100%) rename packages/{openid4vc-verifier/src => openid4vc/src/openid4vc-verifier}/index.ts (63%) rename packages/{openid4vc-verifier/src => openid4vc/src/openid4vc-verifier}/utils.ts (100%) create mode 100644 packages/openid4vc/tests/setup.ts rename packages/{openid4vc-holder => openid4vc}/tsconfig.build.json (100%) rename packages/{openid4vc-holder => openid4vc}/tsconfig.json (100%) diff --git a/demo-openid/package.json b/demo-openid/package.json index d028c947ca..a105d429c5 100644 --- a/demo-openid/package.json +++ b/demo-openid/package.json @@ -15,6 +15,7 @@ "refresh": "rm -rf ./node_modules ./yarn.lock && yarn" }, "dependencies": { + "@aries-framework/openid4vc": "*", "@hyperledger/anoncreds-nodejs": "^0.2.0-dev.4", "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", "@hyperledger/indy-vdr-nodejs": "^0.2.0-dev.5", @@ -25,9 +26,6 @@ "@aries-framework/askar": "*", "@aries-framework/core": "*", "@aries-framework/node": "*", - "@aries-framework/openid4vc-holder": "*", - "@aries-framework/openid4vc-issuer": "*", - "@aries-framework/openid4vc-verifier": "*", "@aries-framework/sd-jwt-vc": "^0.4.2", "@types/express": "^4.17.13", "@types/figlet": "^1.5.4", diff --git a/demo-openid/src/Holder.ts b/demo-openid/src/Holder.ts index d922908c2a..3240c2125c 100644 --- a/demo-openid/src/Holder.ts +++ b/demo-openid/src/Holder.ts @@ -3,10 +3,10 @@ import type { OfferedCredentialWithMetadata, ResolvedPresentationRequest, ResolvedCredentialOffer, -} from '@aries-framework/openid4vc-holder' +} from '@aries-framework/openid4vc' import { AskarModule } from '@aries-framework/askar' -import { OpenId4VcHolderModule } from '@aries-framework/openid4vc-holder' +import { OpenId4VcHolderModule } from '@aries-framework/openid4vc' import { SdJwtVcModule, type SdJwtVcRecord } from '@aries-framework/sd-jwt-vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' diff --git a/demo-openid/src/HolderInquirer.ts b/demo-openid/src/HolderInquirer.ts index c9e009841f..52815db3f3 100644 --- a/demo-openid/src/HolderInquirer.ts +++ b/demo-openid/src/HolderInquirer.ts @@ -1,6 +1,5 @@ -import type { ResolvedCredentialOffer, ResolvedPresentationRequest } from '@aries-framework/openid4vc-holder' +import type { ResolvedCredentialOffer, ResolvedPresentationRequest } from '@aries-framework/openid4vc' -import { OpenIdCredentialFormatProfile } from '@aries-framework/openid4vc-issuer' import { clear } from 'console' import { textSync } from 'figlet' import { prompt } from 'inquirer' diff --git a/demo-openid/src/Issuer.ts b/demo-openid/src/Issuer.ts index d73dac0ffd..90a2cd3a05 100644 --- a/demo-openid/src/Issuer.ts +++ b/demo-openid/src/Issuer.ts @@ -1,14 +1,14 @@ import type { CredentialRequestToCredentialMapper, CredentialSupported, - EndpointConfig, OfferedCredential, -} from '@aries-framework/openid4vc-issuer' + IssuerEndpointConfig, +} from '@aries-framework/openid4vc' import type e from 'express' import { AskarModule } from '@aries-framework/askar' import { W3cCredential, W3cCredentialSubject, W3cIssuer, w3cDate } from '@aries-framework/core' -import { OpenId4VcIssuerModule, OpenIdCredentialFormatProfile } from '@aries-framework/openid4vc-issuer' +import { OpenId4VcIssuerModule, OpenIdCredentialFormatProfile } from '@aries-framework/openid4vc' import { SdJwtVcModule } from '@aries-framework/sd-jwt-vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { Router } from 'express' @@ -70,7 +70,7 @@ export class Issuer extends BaseAgent> } public async configureRouter(): Promise { - const endpointConfig: EndpointConfig = { + const endpointConfig: IssuerEndpointConfig = { metadataEndpointConfig: { enabled: true }, accessTokenEndpointConfig: { enabled: true, diff --git a/demo-openid/src/Verifier.ts b/demo-openid/src/Verifier.ts index 3234fb80c8..3436f98001 100644 --- a/demo-openid/src/Verifier.ts +++ b/demo-openid/src/Verifier.ts @@ -1,15 +1,13 @@ import type { ProofResponseHandler, CreateProofRequestOptions, - EndpointConfig, + VerifierEndpointConfig, PresentationDefinitionV2, -} from '@aries-framework/openid4vc-verifier' +} from '@aries-framework/openid4vc' import type e from 'express' import { AskarModule } from '@aries-framework/askar' -import { SigningAlgo } from '@aries-framework/openid4vc-verifier' -import { OpenId4VcVerifierModule } from '@aries-framework/openid4vc-verifier/src/OpenId4VcVerifierModule' -import { staticOpOpenIdConfig } from '@aries-framework/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions' +import { SigningAlgo, OpenId4VcVerifierModule, staticOpOpenIdConfig } from '@aries-framework/openid4vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { Router } from 'express' @@ -75,7 +73,7 @@ export class Verifier extends BaseAgent { - const endpointConfig: EndpointConfig = { + const endpointConfig: VerifierEndpointConfig = { verificationEndpointConfig: { enabled: true, verificationEndpointPath: Verifier.verificationEndpointPath, diff --git a/packages/openid4vc-holder/package.json b/packages/openid4vc-holder/package.json deleted file mode 100644 index 38a3e1de35..0000000000 --- a/packages/openid4vc-holder/package.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "@aries-framework/openid4vc-holder", - "main": "build/index", - "types": "build/index", - "version": "0.4.2", - "files": [ - "build" - ], - "license": "Apache-2.0", - "publishConfig": { - "access": "public" - }, - "homepage": "https://github.com/hyperledger/aries-framework-javascript/tree/main/packages/openid4vc-holder", - "repository": { - "type": "git", - "url": "https://github.com/hyperledger/aries-framework-javascript", - "directory": "packages/openid4vc-holder" - }, - "scripts": { - "build": "yarn run clean && yarn run compile", - "clean": "rimraf ./build", - "compile": "tsc -p tsconfig.build.json", - "prepublishOnly": "yarn run build", - "test": "jest --forceExit --detectOpenHandles" - }, - "dependencies": { - "@aries-framework/askar": "^0.4.2", - "@aries-framework/core": "0.4.2", - "@aries-framework/sd-jwt-vc": "^0.4.2", - "@aries-framework/openid4vc-verifier": "0.4.2", - "@aries-framework/openid4vc-issuer": "0.4.2" - }, - "devDependencies": { - "@aries-framework/node": "^0.4.2", - "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", - "@types/express": "^4.17.21", - "express": "^4.18.2", - "nock": "^13.3.0", - "rimraf": "^4.4.0", - "typescript": "~4.9.5" - } -} diff --git a/packages/openid4vc-holder/src/issuance.ts b/packages/openid4vc-holder/src/issuance.ts deleted file mode 100644 index ab19cafa31..0000000000 --- a/packages/openid4vc-holder/src/issuance.ts +++ /dev/null @@ -1,19 +0,0 @@ -import type { Issuance } from '@aries-framework/openid4vc-issuer' - -export type AcceptCredentialOfferOptions = Issuance.AcceptCredentialOfferOptions -export type OfferedCredentialWithMetadata = Issuance.OfferedCredentialWithMetadata -export type AuthCodeFlowOptions = Issuance.AuthCodeFlowOptions -export type AuthDetails = Issuance.AuthorizationDetails -export type CredentialOfferPayloadV1_0_11 = Issuance.CredentialOfferPayloadV1_0_11 -export type EndpointMetadataResult = Issuance.EndpointMetadataResult -export type OfferedCredentialType = Issuance.OfferedCredentialType -export type OpenId4VCIVersion = Issuance.OpenId4VCIVersion -export type ProofOfPossessionRequirements = Issuance.ProofOfPossessionRequirements -export type OpenIdCredentialFormatProfile = Issuance.OpenIdCredentialFormatProfile -export type ProofOfPossessionVerificationMethodResolver = Issuance.ProofOfPossessionVerificationMethodResolver -export type ProofOfPossessionVerificationMethodResolverOptions = - Issuance.ProofOfPossessionVerificationMethodResolverOptions -export type ResolvedAuthorizationRequest = Issuance.ResolvedAuthorizationRequest -export type ResolvedAuthorizationRequestWithCode = Issuance.ResolvedAuthorizationRequestWithCode -export type ResolvedCredentialOffer = Issuance.ResolvedCredentialOffer -export type SupportedCredentialFormats = Issuance.SupportedCredentialFormats diff --git a/packages/openid4vc-holder/src/presentation.ts b/packages/openid4vc-holder/src/presentation.ts deleted file mode 100644 index ed8738db10..0000000000 --- a/packages/openid4vc-holder/src/presentation.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Presentation } from '@aries-framework/openid4vc-verifier' - -export type AuthenticationRequest = Presentation.AuthenticationRequest -export type PresentationRequest = Presentation.PresentationRequest -export type PresentationSubmission = Presentation.PresentationSubmission -export type ProofSubmissionResponse = Presentation.ProofSubmissionResponse -export type ResolvedProofRequest = Presentation.ResolvedProofRequest -export type SubmissionEntry = Presentation.SubmissionEntry -export type VpFormat = Presentation.VpFormat -export type ResolvedAuthenticationRequest = Presentation.ResolvedAuthenticationRequest -export type ResolvedPresentationRequest = Presentation.ResolvedPresentationRequest diff --git a/packages/openid4vc-issuer/jest.config.ts b/packages/openid4vc-issuer/jest.config.ts deleted file mode 100644 index 8641cf4d67..0000000000 --- a/packages/openid4vc-issuer/jest.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Config } from '@jest/types' - -import base from '../../jest.config.base' - -import packageJson from './package.json' - -const config: Config.InitialOptions = { - ...base, - - displayName: packageJson.name, - setupFilesAfterEnv: ['./tests/setup.ts'], -} - -export default config diff --git a/packages/openid4vc-issuer/package.json b/packages/openid4vc-issuer/package.json deleted file mode 100644 index 45767d07de..0000000000 --- a/packages/openid4vc-issuer/package.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "@aries-framework/openid4vc-issuer", - "main": "build/index", - "types": "build/index", - "version": "0.4.2", - "files": [ - "build" - ], - "license": "Apache-2.0", - "publishConfig": { - "access": "public" - }, - "homepage": "https://github.com/hyperledger/aries-framework-javascript/tree/main/packages/openid4vc-issuer", - "repository": { - "type": "git", - "url": "https://github.com/hyperledger/aries-framework-javascript", - "directory": "packages/openid4vc-issuer" - }, - "scripts": { - "build": "yarn run clean && yarn run compile", - "clean": "rimraf ./build", - "compile": "tsc -p tsconfig.build.json", - "prepublishOnly": "yarn run build", - "test": "jest" - }, - "dependencies": { - "@aries-framework/askar": "^0.4.2", - "@aries-framework/core": "0.4.2", - "@sphereon/ssi-types": "^0.17.5", - "body-parser": "^1.20.2" - }, - "devDependencies": { - "@aries-framework/node": "^0.4.2", - "@aries-framework/sd-jwt-vc": "^0.4.2", - "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", - "@types/body-parser": "^1.19.5", - "@types/express": "^4.17.21", - "nock": "^13.3.0", - "rimraf": "^4.4.0", - "typescript": "~4.9.5" - } -} diff --git a/packages/openid4vc-issuer/tsconfig.build.json b/packages/openid4vc-issuer/tsconfig.build.json deleted file mode 100644 index 2b075bbd85..0000000000 --- a/packages/openid4vc-issuer/tsconfig.build.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.build.json", - "compilerOptions": { - "outDir": "./build", - "skipLibCheck": true - }, - "include": ["src/**/*"] -} diff --git a/packages/openid4vc-issuer/tsconfig.json b/packages/openid4vc-issuer/tsconfig.json deleted file mode 100644 index c1aca0e050..0000000000 --- a/packages/openid4vc-issuer/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "types": ["jest"], - "skipLibCheck": true - } -} diff --git a/packages/openid4vc-verifier/jest.config.ts b/packages/openid4vc-verifier/jest.config.ts deleted file mode 100644 index 8641cf4d67..0000000000 --- a/packages/openid4vc-verifier/jest.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Config } from '@jest/types' - -import base from '../../jest.config.base' - -import packageJson from './package.json' - -const config: Config.InitialOptions = { - ...base, - - displayName: packageJson.name, - setupFilesAfterEnv: ['./tests/setup.ts'], -} - -export default config diff --git a/packages/openid4vc-verifier/tsconfig.build.json b/packages/openid4vc-verifier/tsconfig.build.json deleted file mode 100644 index 2b075bbd85..0000000000 --- a/packages/openid4vc-verifier/tsconfig.build.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.build.json", - "compilerOptions": { - "outDir": "./build", - "skipLibCheck": true - }, - "include": ["src/**/*"] -} diff --git a/packages/openid4vc-verifier/tsconfig.json b/packages/openid4vc-verifier/tsconfig.json deleted file mode 100644 index c1aca0e050..0000000000 --- a/packages/openid4vc-verifier/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "types": ["jest"], - "skipLibCheck": true - } -} diff --git a/packages/openid4vc-holder/README.md b/packages/openid4vc/README.md similarity index 100% rename from packages/openid4vc-holder/README.md rename to packages/openid4vc/README.md diff --git a/packages/openid4vc-holder/jest.config.ts b/packages/openid4vc/jest.config.ts similarity index 100% rename from packages/openid4vc-holder/jest.config.ts rename to packages/openid4vc/jest.config.ts diff --git a/packages/openid4vc-verifier/package.json b/packages/openid4vc/package.json similarity index 64% rename from packages/openid4vc-verifier/package.json rename to packages/openid4vc/package.json index b01b4b0e3d..530ec1c53b 100644 --- a/packages/openid4vc-verifier/package.json +++ b/packages/openid4vc/package.json @@ -1,5 +1,5 @@ { - "name": "@aries-framework/openid4vc-verifier", + "name": "@aries-framework/openid4vc", "main": "build/index", "types": "build/index", "version": "0.4.2", @@ -10,36 +10,40 @@ "publishConfig": { "access": "public" }, - "homepage": "https://github.com/hyperledger/aries-framework-javascript/tree/main/packages/openid4vc-verifier", + "homepage": "https://github.com/hyperledger/aries-framework-javascript/tree/main/packages/openid4vc", "repository": { "type": "git", "url": "https://github.com/hyperledger/aries-framework-javascript", - "directory": "packages/openid4vc-verifier" + "directory": "packages/openid4vc" }, "scripts": { "build": "yarn run clean && yarn run compile", "clean": "rimraf ./build", "compile": "tsc -p tsconfig.build.json", "prepublishOnly": "yarn run build", - "test": "jest" + "test": "jest --forceExit --detectOpenHandles" }, "dependencies": { "@aries-framework/askar": "^0.4.2", "@aries-framework/core": "0.4.2", + "@sphereon/ssi-types": "^0.17.5", + "@sphereon/oid4vci-client": "file:./../../../Sphereon/sphereon-oidvci-client-0.8.1", + "@sphereon/oid4vci-common": "file:./../../../Sphereon/sphereon-oid4vci-common-0.8.1", + "@sphereon/oid4vci-issuer-server": "file:./../../../Sphereon/sphereon-oid4vci-issuer-server-0.8.1", + "@sphereon/oid4vci-issuer": "file:./../../../Sphereon/sphereon-oid4vci-issuer-0.8.1", "@sphereon/did-auth-siop": "^0.5.0-unstable.7", "@sphereon/pex": "2.2.0", "@sphereon/pex-models": "^2.1.1", - "@sphereon/ssi-types": "^0.17.5", - "@types/jsonpath": "^0.2.4", - "body-parser": "^1.20.2" + "body-parser": "^1.20.2", + "jsonpath": "^1.1.1" }, "devDependencies": { "@aries-framework/node": "^0.4.2", "@aries-framework/sd-jwt-vc": "^0.4.2", "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", - "@types/body-parser": "^1.19.5", "@types/express": "^4.17.21", - "jsonpath": "1.1.1", + "@types/jsonpath": "^0.2.4", + "express": "^4.18.2", "nock": "^13.3.0", "rimraf": "^4.4.0", "typescript": "~4.9.5" diff --git a/packages/openid4vc/src/index.ts b/packages/openid4vc/src/index.ts new file mode 100644 index 0000000000..11b105d20d --- /dev/null +++ b/packages/openid4vc/src/index.ts @@ -0,0 +1,3 @@ +export * from './openid4vc-holder' +export * from './openid4vc-verifier' +export * from './openid4vc-issuer' diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts similarity index 97% rename from packages/openid4vc-holder/src/OpenId4VcHolderApi.ts rename to packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts index d7e08fb571..db94431229 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderApi.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts @@ -1,17 +1,18 @@ +import type { AuthenticationRequest, PresentationRequest, PresentationSubmission } from './presentation' import type { ResolvedCredentialOffer, ResolvedAuthorizationRequest, AuthCodeFlowOptions, AcceptCredentialOfferOptions, CredentialOfferPayloadV1_0_11, -} from './issuance' -import type { AuthenticationRequest, PresentationRequest, PresentationSubmission } from './presentation' +} from './reception' import type { VerificationMethod, W3cCredentialRecord } from '@aries-framework/core' import type { SdJwtVcRecord } from '@aries-framework/sd-jwt-vc' import { injectable, AgentContext } from '@aries-framework/core' -import { OpenId4VciHolderService } from '@aries-framework/openid4vc-issuer' -import { OpenId4VpHolderService } from '@aries-framework/openid4vc-verifier' + +import { OpenId4VpHolderService } from './presentation' +import { OpenId4VciHolderService } from './reception' /** * @public diff --git a/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderModule.ts similarity index 91% rename from packages/openid4vc-holder/src/OpenId4VcHolderModule.ts rename to packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderModule.ts index 5292a2b0d2..a141f8b74f 100644 --- a/packages/openid4vc-holder/src/OpenId4VcHolderModule.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderModule.ts @@ -1,10 +1,10 @@ import type { DependencyManager, Module } from '@aries-framework/core' import { AgentConfig } from '@aries-framework/core' -import { OpenId4VciHolderService } from '@aries-framework/openid4vc-issuer' -import { OpenId4VpHolderService, PresentationExchangeService } from '@aries-framework/openid4vc-verifier' import { OpenId4VcHolderApi } from './OpenId4VcHolderApi' +import { OpenId4VpHolderService, PresentationExchangeService } from './presentation' +import { OpenId4VciHolderService } from './reception' /** * @public @module OpenId4VcHolderModule diff --git a/packages/openid4vc/src/openid4vc-holder/README.md b/packages/openid4vc/src/openid4vc-holder/README.md new file mode 100644 index 0000000000..04917ab764 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-holder/README.md @@ -0,0 +1,177 @@ +

+
+ Hyperledger Aries logo +

+

Aries Framework JavaScript Open ID Connect For Verifiable Credentials Client Module

+

+ License + typescript + @aries-framework/openid4vc-holder version + +

+
+ +Open ID Connect For Verifiable Credentials Holder Module for [Aries Framework JavaScript](https://github.com/hyperledger/aries-framework-javascript). + +### Installation + +Make sure you have set up the correct version of Aries Framework JavaScript according to the AFJ repository. + +```sh +yarn add @aries-framework/openid4vc-holder +``` + +### Quick start + +#### Requirements + +Before a credential can be requested, you need the issuer URI. This URI starts with `openid-initiate-issuance://` and is provided by the issuer. The issuer URI is commonly acquired by scanning a QR code. + +#### Module registration + +In order to get this module to work, we need to inject it into the agent. This makes the module's functionality accessible through the agent's `modules` api. + +```ts +import { OpenId4VcHolderModule } from '@aries-framework/openid4vc-holder' + +const agent = new Agent({ + config: { + /* config */ + }, + dependencies: agentDependencies, + modules: { + openId4VcHolder: new OpenId4VcHolderModule(), + /* other custom modules */ + }, +}) + +await agent.initialize() +``` + +How the module is injected and the agent has been initialized, you can access the module's functionality through `agent.modules.openId4VcHolder`. + +#### Preparing a DID + +In order to request a credential, you'll need to provide a DID that the issuer will use for setting the credential subject. In the following snippet we create one for the sake of the example, but this can be any DID that has a _authentication verification method_ with key type `Ed25519`. + +```ts +// first we create the DID +const did = await agent.dids.create({ + method: 'key', + options: { + keyType: KeyType.Ed25519, + }, +}) + +// next we do some assertions and extract the key identifier (kid) +if ( + !did.didState.didDocument || + !did.didState.didDocument.authentication || + did.didState.didDocument.authentication.length === 0 +) { + throw new Error("Error creating did document, or did document has no 'authentication' verificationMethods") +} + +const [verificationMethod] = did.didState.didDocument.authentication +const kid = typeof verificationMethod === 'string' ? verificationMethod : verificationMethod.id +``` + +#### Requesting the credential (Pre-Authorized) + +```ts +// To request credentials(s), you need a credential offer. +// The credential offer be provided as actual payload, +// the credential offer URL or issuance initiation URL +const credentialOffer = + 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fjff.walt.id%2Fissuer-api%2Fdefault%2Foidc%22%2C%22credentials%22%3A%5B%22VerifiableId%22%2C%20%22VerifiableDiploma%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22ABC%22%7D%7D%7D' + +// The first step is to resolve the credential offer and +// get all metadata required for the issuance of the credentials. +const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) + +// The second (optional) step is to filter out the credentials which you want to request. +const selectedCredentialsForRequest = resolvedCredentialOffer.credentialsToRequest.filter((credential) => { + return credential.format === OpenIdCredentialFormatProfile.JwtVcJson && credential.types.includes('VerifiableId') +}) + +// The third step is to accept the credential offer. +// If no credentialsToRequest are specified all offered credentials are requested. +const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + resolvedCredentialOffer, + { + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], + proofOfPossessionVerificationMethodResolver: () => verificationMethod, + verifyCredentialStatus: false, + credentialsToRequest: selectedCredentialsForRequest, + } +) + +console.log(w3cCredentialRecords) +``` + +#### Requesting the credential (Authorization Code Flow) + +Requesting credentials via the Authorization Code Flow function conceptually similar, +except that there is an intermediary step involved to resolve the authorization request, and then manually get the authorization code. + +```ts +// To request credentials(s), you need a credential offer. +// The credential offer be provided as actual payload, +// the credential offer URL or issuance initiation URL +const credentialOffer = `openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.portal.walt.id%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22OpenBadgeCredential%22%5D%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fpurl.imsglobal.org%2Fspec%2Fob%2Fv3p0%2Fcontext.json%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22OpenBadgeCredential%22%5D%7D%7D%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22b0e16785-d722-42a5-a04f-4beab28e03ea%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJiMGUxNjc4NS1kNzIyLTQyYTUtYTA0Zi00YmVhYjI4ZTAzZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.ibEpHFaHFBLWyhEf4SotDQZBeh_FMrfncWapNox1Iv1kdQWQ2cLQeS1VrCyVmPsbx0tN2MAyDFG7DnAaq8MiAA%22%2C%22user_pin_required%22%3Afalse%7D%7D%7D` + +// The first step is to resolve the credential offer and +// get all metadata required for the issuance of the credentials. +const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) + +// The second step is the resolve the authorization request. +const resolvedAuthorizationRequest = await agent.modules.openId4VcHolder.resolveAuthorizationRequest(resolved, { + clientId: 'test-client', + redirectUri: 'http://blank', + scope: ['openid', 'OpenBadgeCredential'], +}) + +// The resolved authorization request contains the authorizationRequestUri, +// which can be used to obtain the actual authorization code. +// Currently, this needs to be done manually +const code = + 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJiMGUxNjc4NS1kNzIyLTQyYTUtYTA0Zi00YmVhYjI4ZTAzZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.ibEpHFaHFBLWyhEf4SotDQZBeh_FMrfncWapNox1Iv1kdQWQ2cLQeS1VrCyVmPsbx0tN2MAyDFG7DnAaq8MiAA' + +// The third (optional) step is to filter out the credentials which you want to request. +const selectedCredentialsForRequest = resolvedCredentialOffer.credentialsToRequest.filter((credential) => { + return credential.format === OpenIdCredentialFormatProfile.JwtVcJson && credential.types.includes('VerifiableId') +}) + +// The fourth step is to accept the credential offer. +// If no credentialsToRequest are specified all offered credentials are requested. +const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( + resolvedCredentialOffer, + resolvedAuthorizationRequest, + code, + { + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], + proofOfPossessionVerificationMethodResolver: () => verificationMethod, + verifyCredentialStatus: false, + credentialsToRequest: selectedCredentialsForRequest, + } +) + +console.log(w3cCredentialRecords) +``` diff --git a/packages/openid4vc-holder/tests/fixtures.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/fixtures.ts similarity index 100% rename from packages/openid4vc-holder/tests/fixtures.ts rename to packages/openid4vc/src/openid4vc-holder/__tests__/fixtures.ts diff --git a/packages/openid4vc-holder/tests/fixtures_vp.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/fixtures_vp.ts similarity index 100% rename from packages/openid4vc-holder/tests/fixtures_vp.ts rename to packages/openid4vc/src/openid4vc-holder/__tests__/fixtures_vp.ts diff --git a/packages/openid4vc-holder/tests/openId4vc-holder-module.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/openId4vc-holder-module.test.ts similarity index 78% rename from packages/openid4vc-holder/tests/openId4vc-holder-module.test.ts rename to packages/openid4vc/src/openid4vc-holder/__tests__/openId4vc-holder-module.test.ts index 7d70b29c8a..54f4f83eb6 100644 --- a/packages/openid4vc-holder/tests/openId4vc-holder-module.test.ts +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/openId4vc-holder-module.test.ts @@ -1,11 +1,9 @@ /* eslint-disable @typescript-eslint/unbound-method */ import type { DependencyManager } from '@aries-framework/core' -import { OpenId4VciHolderService } from '@aries-framework/openid4vc-issuer' -import { OpenId4VpHolderService, PresentationExchangeService } from '@aries-framework/openid4vc-verifier' - -import { OpenId4VcHolderApi } from '../src/OpenId4VcHolderApi' -import { OpenId4VcHolderModule } from '../src/OpenId4VcHolderModule' +import { OpenId4VciHolderService, OpenId4VpHolderService, PresentationExchangeService } from '..' +import { OpenId4VcHolderApi } from '../OpenId4VcHolderApi' +import { OpenId4VcHolderModule } from '../OpenId4VcHolderModule' const dependencyManager = { registerInstance: jest.fn(), diff --git a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts similarity index 99% rename from packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts rename to packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts index 4a343fb4cf..e811fee5aa 100644 --- a/packages/openid4vc-holder/tests/openid4vci-holder.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts @@ -1,33 +1,30 @@ +import type { CredentialSupported, IssuerMetadata, PreAuthorizedCodeFlowConfig } from '../../openid4vc-issuer' import type { KeyDidCreateOptions, VerificationMethod } from '@aries-framework/core' -import type { - CredentialSupported, - IssuerMetadata, - PreAuthorizedCodeFlowConfig, -} from '@aries-framework/openid4vc-issuer' import type { Server } from 'http' import { AskarModule } from '@aries-framework/askar' import { - JwaSignatureAlgorithm, Agent, + ClaimFormat, + DidKey, + JwaSignatureAlgorithm, KeyType, TypedArrayEncoder, - W3cCredentialRecord, - DidKey, - ClaimFormat, W3cCredential, - W3cIssuer, + W3cCredentialRecord, W3cCredentialSubject, + W3cIssuer, w3cDate, } from '@aries-framework/core' import { agentDependencies } from '@aries-framework/node' -import { OpenId4VcIssuerModule, OpenIdCredentialFormatProfile } from '@aries-framework/openid4vc-issuer' import { SdJwtVcModule } from '@aries-framework/sd-jwt-vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import express, { Router, type Express } from 'express' import nock, { cleanAll, enableNetConnect } from 'nock' -import { OpenId4VcHolderModule } from '../src/OpenId4VcHolderModule' +import { OpenIdCredentialFormatProfile } from '..' +import { OpenId4VcIssuerModule } from '../../openid4vc-issuer' +import { OpenId4VcHolderModule } from '../OpenId4VcHolderModule' import { mattrLaunchpadJsonLd_draft_08, diff --git a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts similarity index 99% rename from packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts rename to packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts index d7090ae3c8..1dcb0c3aec 100644 --- a/packages/openid4vc-holder/tests/openid4vp-holder.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts @@ -1,5 +1,5 @@ +import type { CreateProofRequestOptions } from '../../openid4vc-verifier' import type { KeyDidCreateOptions, VerificationMethod } from '@aries-framework/core' -import type { CreateProofRequestOptions } from '@aries-framework/openid4vc-verifier' import type { PresentationDefinitionV2 } from '@sphereon/pex-models' import type { Express } from 'express' import type { Server } from 'http' @@ -7,12 +7,12 @@ import type { Server } from 'http' import { AskarModule } from '@aries-framework/askar' import { Agent, DidKey, KeyType, TypedArrayEncoder, W3cJwtVerifiableCredential } from '@aries-framework/core' import { agentDependencies } from '@aries-framework/node' -import { OpenId4VcVerifierModule, SigningAlgo, staticOpOpenIdConfig } from '@aries-framework/openid4vc-verifier' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import express, { Router } from 'express' import nock from 'nock' -import { OpenId4VcHolderModule } from '../src' +import { OpenId4VcHolderModule } from '..' +import { OpenId4VcVerifierModule, SigningAlgo, staticOpOpenIdConfig } from '../../openid4vc-verifier' import { waltPortalOpenBadgeJwt, waltUniversityDegreeJwt } from './fixtures_vp' diff --git a/packages/openid4vc-holder/tests/setup.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/setup.ts similarity index 100% rename from packages/openid4vc-holder/tests/setup.ts rename to packages/openid4vc/src/openid4vc-holder/__tests__/setup.ts diff --git a/packages/openid4vc-holder/src/index.ts b/packages/openid4vc/src/openid4vc-holder/index.ts similarity index 79% rename from packages/openid4vc-holder/src/index.ts rename to packages/openid4vc/src/openid4vc-holder/index.ts index ef6371a392..fd3fb09683 100644 --- a/packages/openid4vc-holder/src/index.ts +++ b/packages/openid4vc/src/openid4vc-holder/index.ts @@ -1,4 +1,4 @@ export * from './OpenId4VcHolderApi' export * from './OpenId4VcHolderModule' -export * from './issuance' export * from './presentation' +export * from './reception' diff --git a/packages/openid4vc-verifier/src/presentation/OpenId4VpHolderService.ts b/packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderService.ts similarity index 98% rename from packages/openid4vc-verifier/src/presentation/OpenId4VpHolderService.ts rename to packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderService.ts index f392c85762..f470ace3b3 100644 --- a/packages/openid4vc-verifier/src/presentation/OpenId4VpHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderService.ts @@ -31,7 +31,11 @@ import { VerificationMode, } from '@sphereon/did-auth-siop' -import { getResolver, getSuppliedSignatureFromVerificationMethod, getSupportedDidMethods } from '../utils' +import { + getResolver, + getSuppliedSignatureFromVerificationMethod, + getSupportedDidMethods, +} from '../../openid4vc-verifier/utils' import { PresentationExchangeService } from './PresentationExchangeService' diff --git a/packages/openid4vc-verifier/src/presentation/OpenId4VpHolderServiceOptions.ts b/packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderServiceOptions.ts similarity index 100% rename from packages/openid4vc-verifier/src/presentation/OpenId4VpHolderServiceOptions.ts rename to packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderServiceOptions.ts diff --git a/packages/openid4vc-verifier/src/presentation/PresentationExchangeService.ts b/packages/openid4vc/src/openid4vc-holder/presentation/PresentationExchangeService.ts similarity index 100% rename from packages/openid4vc-verifier/src/presentation/PresentationExchangeService.ts rename to packages/openid4vc/src/openid4vc-holder/presentation/PresentationExchangeService.ts diff --git a/packages/openid4vc-verifier/src/presentation/index.ts b/packages/openid4vc/src/openid4vc-holder/presentation/index.ts similarity index 100% rename from packages/openid4vc-verifier/src/presentation/index.ts rename to packages/openid4vc/src/openid4vc-holder/presentation/index.ts diff --git a/packages/openid4vc-verifier/src/presentation/selection/PexCredentialSelection.ts b/packages/openid4vc/src/openid4vc-holder/presentation/selection/PexCredentialSelection.ts similarity index 100% rename from packages/openid4vc-verifier/src/presentation/selection/PexCredentialSelection.ts rename to packages/openid4vc/src/openid4vc-holder/presentation/selection/PexCredentialSelection.ts diff --git a/packages/openid4vc-verifier/src/presentation/selection/example.md b/packages/openid4vc/src/openid4vc-holder/presentation/selection/example.md similarity index 100% rename from packages/openid4vc-verifier/src/presentation/selection/example.md rename to packages/openid4vc/src/openid4vc-holder/presentation/selection/example.md diff --git a/packages/openid4vc-verifier/src/presentation/selection/index.ts b/packages/openid4vc/src/openid4vc-holder/presentation/selection/index.ts similarity index 100% rename from packages/openid4vc-verifier/src/presentation/selection/index.ts rename to packages/openid4vc/src/openid4vc-holder/presentation/selection/index.ts diff --git a/packages/openid4vc-verifier/src/presentation/selection/types.ts b/packages/openid4vc/src/openid4vc-holder/presentation/selection/types.ts similarity index 100% rename from packages/openid4vc-verifier/src/presentation/selection/types.ts rename to packages/openid4vc/src/openid4vc-holder/presentation/selection/types.ts diff --git a/packages/openid4vc-verifier/src/presentation/transform.ts b/packages/openid4vc/src/openid4vc-holder/presentation/transform.ts similarity index 100% rename from packages/openid4vc-verifier/src/presentation/transform.ts rename to packages/openid4vc/src/openid4vc-holder/presentation/transform.ts diff --git a/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderService.ts b/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderService.ts similarity index 100% rename from packages/openid4vc-issuer/src/issuance/OpenId4VciHolderService.ts rename to packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderService.ts diff --git a/packages/openid4vc-issuer/src/issuance/OpenId4VciHolderServiceOptions.ts b/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderServiceOptions.ts similarity index 100% rename from packages/openid4vc-issuer/src/issuance/OpenId4VciHolderServiceOptions.ts rename to packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderServiceOptions.ts diff --git a/packages/openid4vc-issuer/src/issuance/index.ts b/packages/openid4vc/src/openid4vc-holder/reception/index.ts similarity index 100% rename from packages/openid4vc-issuer/src/issuance/index.ts rename to packages/openid4vc/src/openid4vc-holder/reception/index.ts diff --git a/packages/openid4vc-issuer/src/issuance/utils/Formats.ts b/packages/openid4vc/src/openid4vc-holder/reception/utils/Formats.ts similarity index 100% rename from packages/openid4vc-issuer/src/issuance/utils/Formats.ts rename to packages/openid4vc/src/openid4vc-holder/reception/utils/Formats.ts diff --git a/packages/openid4vc-issuer/src/issuance/utils/IssuerMetadataUtils.ts b/packages/openid4vc/src/openid4vc-holder/reception/utils/IssuerMetadataUtils.ts similarity index 100% rename from packages/openid4vc-issuer/src/issuance/utils/IssuerMetadataUtils.ts rename to packages/openid4vc/src/openid4vc-holder/reception/utils/IssuerMetadataUtils.ts diff --git a/packages/openid4vc-issuer/src/issuance/utils/__tests__/claimFormatMapping.test.ts b/packages/openid4vc/src/openid4vc-holder/reception/utils/__tests__/claimFormatMapping.test.ts similarity index 100% rename from packages/openid4vc-issuer/src/issuance/utils/__tests__/claimFormatMapping.test.ts rename to packages/openid4vc/src/openid4vc-holder/reception/utils/__tests__/claimFormatMapping.test.ts diff --git a/packages/openid4vc-issuer/src/issuance/utils/claimFormatMapping.ts b/packages/openid4vc/src/openid4vc-holder/reception/utils/claimFormatMapping.ts similarity index 100% rename from packages/openid4vc-issuer/src/issuance/utils/claimFormatMapping.ts rename to packages/openid4vc/src/openid4vc-holder/reception/utils/claimFormatMapping.ts diff --git a/packages/openid4vc-issuer/src/issuance/utils/index.ts b/packages/openid4vc/src/openid4vc-holder/reception/utils/index.ts similarity index 100% rename from packages/openid4vc-issuer/src/issuance/utils/index.ts rename to packages/openid4vc/src/openid4vc-holder/reception/utils/index.ts diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts similarity index 95% rename from packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts rename to packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts index aff712448d..fb6d72d071 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerApi.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts @@ -3,9 +3,9 @@ import type { CreateCredentialOfferAndRequestOptions, CredentialOfferAndRequest, OfferedCredential, - CredentialOfferPayloadV1_0_11, - EndpointConfig, + IssuerEndpointConfig, } from './OpenId4VcIssuerServiceOptions' +import type { CredentialOfferPayloadV1_0_11 } from '@sphereon/oid4vci-common' import type { Router } from 'express' import { injectable, AgentContext } from '@aries-framework/core' @@ -84,7 +84,7 @@ export class OpenId4VcIssuerApi { * @param endpointConfig - The endpoint configuration. * @returns The configured router. */ - public async configureRouter(router: Router, endpointConfig: EndpointConfig) { + public async configureRouter(router: Router, endpointConfig: IssuerEndpointConfig) { return this.openId4VcIssuerService.configureRouter(this.agentContext, router, endpointConfig) } } diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerModule.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts similarity index 100% rename from packages/openid4vc-issuer/src/OpenId4VcIssuerModule.ts rename to packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerModuleConfig.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts similarity index 100% rename from packages/openid4vc-issuer/src/OpenId4VcIssuerModuleConfig.ts rename to packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts similarity index 98% rename from packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts rename to packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts index 7ec716dc6e..ac4787e652 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerService.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts @@ -6,9 +6,9 @@ import type { CreateIssueCredentialResponseOptions, CredentialSupported, CredentialOfferAndRequest, - EndpointConfig, + IssuerEndpointConfig, } from './OpenId4VcIssuerServiceOptions' -import type { OfferedCredentialWithMetadata } from './issuance/utils/IssuerMetadataUtils' +import type { OfferedCredentialWithMetadata } from '../openid4vc-holder/reception/utils/IssuerMetadataUtils' import type { AgentContext, VerificationMethod, @@ -53,9 +53,10 @@ import { IssueStatus } from '@sphereon/oid4vci-common' import { VcIssuerBuilder } from '@sphereon/oid4vci-issuer' import bodyParser from 'body-parser' +import { OpenIdCredentialFormatProfile } from '../openid4vc-holder' +import { getOfferedCredentialsWithMetadata } from '../openid4vc-holder/reception/utils/IssuerMetadataUtils' + import { OpenId4VcIssuerModuleConfig } from './OpenId4VcIssuerModuleConfig' -import { OpenIdCredentialFormatProfile } from './issuance' -import { getOfferedCredentialsWithMetadata } from './issuance/utils/IssuerMetadataUtils' import { configureAccessTokenEndpoint, configureCredentialEndpoint, @@ -454,7 +455,7 @@ export class OpenId4VcIssuerService { return issueCredentialResponse } - public configureRouter = (agentContext: AgentContext, router: Router, endpointConfig: EndpointConfig) => { + public configureRouter = (agentContext: AgentContext, router: Router, endpointConfig: IssuerEndpointConfig) => { // parse application/x-www-form-urlencoded router.use(bodyParser.urlencoded({ extended: false })) // parse application/json diff --git a/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts similarity index 95% rename from packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts rename to packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts index 53435ce427..918460a849 100644 --- a/packages/openid4vc-issuer/src/OpenId4VcIssuerServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts @@ -10,13 +10,7 @@ import type { ProofOfPossession, } from '@sphereon/oid4vci-common' -export type { - MetadataDisplay, - ProofOfPossession, - CredentialOfferPayloadV1_0_11, - CredentialSupported, - CredentialOfferFormat, -} +export type { MetadataDisplay, ProofOfPossession, CredentialSupported, CredentialOfferFormat } // If the entry is an object, the object contains the data related to a certain credential type // the Wallet MAY request. Each object MUST contain a format Claim determining the format @@ -133,7 +127,7 @@ export interface CredentialEndpointConfig { credentialRequestToCredentialMapper: CredentialRequestToCredentialMapper } -export interface EndpointConfig { +export interface IssuerEndpointConfig { metadataEndpointConfig?: MetadataEndpointConfig accessTokenEndpointConfig?: AccessTokenEndpointConfig credentialEndpointConfig?: CredentialEndpointConfig diff --git a/packages/openid4vc-issuer/README.md b/packages/openid4vc/src/openid4vc-issuer/README.md similarity index 100% rename from packages/openid4vc-issuer/README.md rename to packages/openid4vc/src/openid4vc-issuer/README.md diff --git a/packages/openid4vc-issuer/tests/openId4vc-issuer-module.test.ts b/packages/openid4vc/src/openid4vc-issuer/__tests__/openId4vc-issuer-module.test.ts similarity index 83% rename from packages/openid4vc-issuer/tests/openId4vc-issuer-module.test.ts rename to packages/openid4vc/src/openid4vc-issuer/__tests__/openId4vc-issuer-module.test.ts index fb26071112..749405ca1f 100644 --- a/packages/openid4vc-issuer/tests/openId4vc-issuer-module.test.ts +++ b/packages/openid4vc/src/openid4vc-issuer/__tests__/openId4vc-issuer-module.test.ts @@ -1,10 +1,10 @@ /* eslint-disable @typescript-eslint/unbound-method */ import type { DependencyManager } from '@aries-framework/core' -import { OpenId4VcIssuerApi } from '../src/OpenId4VcIssuerApi' -import { OpenId4VcIssuerModule } from '../src/OpenId4VcIssuerModule' -import { OpenId4VcIssuerModuleConfig } from '../src/OpenId4VcIssuerModuleConfig' -import { OpenId4VcIssuerService } from '../src/OpenId4VcIssuerService' +import { OpenId4VcIssuerApi } from '../OpenId4VcIssuerApi' +import { OpenId4VcIssuerModule } from '../OpenId4VcIssuerModule' +import { OpenId4VcIssuerModuleConfig } from '../OpenId4VcIssuerModuleConfig' +import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' const dependencyManager = { registerInstance: jest.fn(), diff --git a/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts b/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.e2e.test.ts similarity index 98% rename from packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts rename to packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.e2e.test.ts index 6839bae39c..272b0ab102 100644 --- a/packages/openid4vc-issuer/tests/openid4vc-issuer.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.e2e.test.ts @@ -1,9 +1,9 @@ import type { - PreAuthorizedCodeFlowConfig, AuthorizationCodeFlowConfig, - IssuerMetadata, CredentialSupported, -} from '../src/OpenId4VcIssuerServiceOptions' + IssuerMetadata, + PreAuthorizedCodeFlowConfig, +} from '../OpenId4VcIssuerServiceOptions' import type { AgentContext, KeyDidCreateOptions, @@ -30,17 +30,18 @@ import { W3cIssuer, W3cJsonLdVerifiableCredential, W3cJwtVerifiableCredential, + equalsIgnoreOrder, getJwkFromKey, getKeyFromVerificationMethod, w3cDate, - equalsIgnoreOrder, } from '@aries-framework/core' import { agentDependencies } from '@aries-framework/node' import { SdJwtVcApi, SdJwtVcModule } from '@aries-framework/sd-jwt-vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { cleanAll, enableNetConnect } from 'nock' -import { OpenIdCredentialFormatProfile, OpenId4VcIssuerModule, OpenId4VcIssuerService } from '../src' +import { OpenId4VcIssuerModule, OpenId4VcIssuerService } from '..' +import { OpenIdCredentialFormatProfile } from '../../openid4vc-holder' type CredentialSupportedWithId = CredentialSupported & { id: string } @@ -165,10 +166,10 @@ describe('OpenId4VcIssuer', () => { beforeEach(async () => { issuer = new Agent({ config: { - label: 'OpenId4VcIssuer Test', + label: 'OpenId4VcIssuer Test321', walletConfig: { - id: 'openid4vc-Issuer-test', - key: 'openid4vc-Issuer-test', + id: 'openid4vc-Issuer-test321', + key: 'openid4vc-Issuer-test321', }, }, dependencies: agentDependencies, @@ -177,10 +178,10 @@ describe('OpenId4VcIssuer', () => { holder = new Agent({ config: { - label: 'OpenId4VciIssuer(Holder) Test', + label: 'OpenId4VciIssuer(Holder) Test321', walletConfig: { - id: 'openid4vc-Issuer(Holder)-test', - key: 'openid4vc-Issuer(Holder)-test', + id: 'openid4vc-Issuer(Holder)-test321', + key: 'openid4vc-Issuer(Holder)-test321', }, }, dependencies: agentDependencies, diff --git a/packages/openid4vc-issuer/tests/setup.ts b/packages/openid4vc/src/openid4vc-issuer/__tests__/setup.ts similarity index 100% rename from packages/openid4vc-issuer/tests/setup.ts rename to packages/openid4vc/src/openid4vc-issuer/__tests__/setup.ts diff --git a/packages/openid4vc-issuer/src/index.ts b/packages/openid4vc/src/openid4vc-issuer/index.ts similarity index 59% rename from packages/openid4vc-issuer/src/index.ts rename to packages/openid4vc/src/openid4vc-issuer/index.ts index eb4c37704b..f99285d628 100644 --- a/packages/openid4vc-issuer/src/index.ts +++ b/packages/openid4vc/src/openid4vc-issuer/index.ts @@ -1,9 +1,3 @@ -import * as Issuance from './issuance' - -export { Issuance } - -export { OpenId4VciHolderService, OpenIdCredentialFormatProfile } from './issuance' - export * from './OpenId4VcIssuerApi' export * from './OpenId4VcIssuerModule' export * from './OpenId4VcIssuerService' diff --git a/packages/openid4vc-issuer/src/router/OpenId4VcIEndpointConfiguration.ts b/packages/openid4vc/src/openid4vc-issuer/router/OpenId4VcIEndpointConfiguration.ts similarity index 100% rename from packages/openid4vc-issuer/src/router/OpenId4VcIEndpointConfiguration.ts rename to packages/openid4vc/src/openid4vc-issuer/router/OpenId4VcIEndpointConfiguration.ts diff --git a/packages/openid4vc-issuer/src/router/accessTokenEndpoint.ts b/packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts similarity index 100% rename from packages/openid4vc-issuer/src/router/accessTokenEndpoint.ts rename to packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts diff --git a/packages/openid4vc-issuer/src/router/utils.ts b/packages/openid4vc/src/openid4vc-issuer/router/utils.ts similarity index 100% rename from packages/openid4vc-issuer/src/router/utils.ts rename to packages/openid4vc/src/openid4vc-issuer/router/utils.ts diff --git a/packages/openid4vc-verifier/src/InMemoryVerifierSessionManager.ts b/packages/openid4vc/src/openid4vc-verifier/InMemoryVerifierSessionManager.ts similarity index 100% rename from packages/openid4vc-verifier/src/InMemoryVerifierSessionManager.ts rename to packages/openid4vc/src/openid4vc-verifier/InMemoryVerifierSessionManager.ts diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierApi.ts similarity index 94% rename from packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts rename to packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierApi.ts index 5d2c46b710..fbc7982379 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierApi.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierApi.ts @@ -1,4 +1,4 @@ -import type { CreateProofRequestOptions, EndpointConfig, ProofPayload } from './OpenId4VcVerifierServiceOptions' +import type { CreateProofRequestOptions, VerifierEndpointConfig, ProofPayload } from './OpenId4VcVerifierServiceOptions' import type { Router } from 'express' import { injectable, AgentContext } from '@aries-framework/core' @@ -58,7 +58,7 @@ export class OpenId4VcVerifierApi { * @param endpointConfig - The endpoint configuration. * @returns The configured router. */ - public async configureRouter(router: Router, endpointConfig: EndpointConfig) { + public async configureRouter(router: Router, endpointConfig: VerifierEndpointConfig) { return await this.openId4VcVerifierService.configureRouter(this.agentContext, router, endpointConfig) } } diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierModule.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts similarity index 100% rename from packages/openid4vc-verifier/src/OpenId4VcVerifierModule.ts rename to packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierModuleConfig.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts similarity index 100% rename from packages/openid4vc-verifier/src/OpenId4VcVerifierModuleConfig.ts rename to packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierService.ts similarity index 99% rename from packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts rename to packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierService.ts index 68cc56b860..0ae8bb27af 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierService.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierService.ts @@ -4,7 +4,7 @@ import type { CreateProofRequestOptions, ProofRequestMetadata, VerifiedProofResponse, - EndpointConfig, + VerifierEndpointConfig, } from './OpenId4VcVerifierServiceOptions' import type { AgentContext, W3cVerifyPresentationResult } from '@aries-framework/core' import type { @@ -316,7 +316,7 @@ export class OpenId4VcVerifierService { } } - public configureRouter = (agentContext: AgentContext, router: Router, endpointConfig: EndpointConfig) => { + public configureRouter = (agentContext: AgentContext, router: Router, endpointConfig: VerifierEndpointConfig) => { // parse application/x-www-form-urlencoded router.use(bodyParser.urlencoded({ extended: false })) diff --git a/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierServiceOptions.ts similarity index 98% rename from packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts rename to packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierServiceOptions.ts index dfaa7710e5..8772c5cd06 100644 --- a/packages/openid4vc-verifier/src/OpenId4VcVerifierServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierServiceOptions.ts @@ -86,6 +86,6 @@ export interface VerificationEndpointConfig { proofResponseHandler?: ProofResponseHandler } -export interface EndpointConfig { +export interface VerifierEndpointConfig { verificationEndpointConfig: VerificationEndpointConfig } diff --git a/packages/openid4vc-verifier/README.md b/packages/openid4vc/src/openid4vc-verifier/README.md similarity index 100% rename from packages/openid4vc-verifier/README.md rename to packages/openid4vc/src/openid4vc-verifier/README.md diff --git a/packages/openid4vc-verifier/tests/openId4vc-verifier-module.test.ts b/packages/openid4vc/src/openid4vc-verifier/__tests__/openId4vc-verifier-module.test.ts similarity index 82% rename from packages/openid4vc-verifier/tests/openId4vc-verifier-module.test.ts rename to packages/openid4vc/src/openid4vc-verifier/__tests__/openId4vc-verifier-module.test.ts index aa2659a35c..699011880b 100644 --- a/packages/openid4vc-verifier/tests/openId4vc-verifier-module.test.ts +++ b/packages/openid4vc/src/openid4vc-verifier/__tests__/openId4vc-verifier-module.test.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/unbound-method */ import type { DependencyManager } from '@aries-framework/core' -import { OpenId4VcVerifierApi } from '../src/OpenId4VcVerifierApi' -import { OpenId4VcVerifierModule } from '../src/OpenId4VcVerifierModule' -import { OpenId4VcVerifierService } from '../src/OpenId4VcVerifierService' +import { OpenId4VcVerifierApi } from '../OpenId4VcVerifierApi' +import { OpenId4VcVerifierModule } from '../OpenId4VcVerifierModule' +import { OpenId4VcVerifierService } from '../OpenId4VcVerifierService' const dependencyManager = { registerInstance: jest.fn(), diff --git a/packages/openid4vc-verifier/tests/openid4vc-verifier.e2e.test.ts b/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.e2e.test.ts similarity index 99% rename from packages/openid4vc-verifier/tests/openid4vc-verifier.e2e.test.ts rename to packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.e2e.test.ts index 8c757750f1..67c08da693 100644 --- a/packages/openid4vc-verifier/tests/openid4vc-verifier.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.e2e.test.ts @@ -1,4 +1,4 @@ -import type { HolderMetadata, PresentationDefinitionV2 } from '../src' +import type { HolderMetadata, PresentationDefinitionV2 } from '..' import type { KeyDidCreateOptions, VerificationMethod } from '@aries-framework/core' import { AskarModule } from '@aries-framework/askar' @@ -8,7 +8,7 @@ import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { SigningAlgo } from '@sphereon/did-auth-siop' import { cleanAll, enableNetConnect } from 'nock' -import { OpenId4VcVerifierModule, staticOpOpenIdConfig, staticOpSiopConfig } from '../src' +import { OpenId4VcVerifierModule, staticOpOpenIdConfig, staticOpSiopConfig } from '..' const modules = { openId4VcVerifier: new OpenId4VcVerifierModule({}), diff --git a/packages/openid4vc-verifier/tests/setup.ts b/packages/openid4vc/src/openid4vc-verifier/__tests__/setup.ts similarity index 100% rename from packages/openid4vc-verifier/tests/setup.ts rename to packages/openid4vc/src/openid4vc-verifier/__tests__/setup.ts diff --git a/packages/openid4vc-verifier/src/index.ts b/packages/openid4vc/src/openid4vc-verifier/index.ts similarity index 63% rename from packages/openid4vc-verifier/src/index.ts rename to packages/openid4vc/src/openid4vc-verifier/index.ts index 56a1c21dfa..24bc2e517c 100644 --- a/packages/openid4vc-verifier/src/index.ts +++ b/packages/openid4vc/src/openid4vc-verifier/index.ts @@ -1,9 +1,3 @@ -import * as Presentation from './presentation' - -export { Presentation } - -export { OpenId4VpHolderService, PresentationExchangeService } from './presentation' - export * from './OpenId4VcVerifierApi' export * from './OpenId4VcVerifierModule' export * from './OpenId4VcVerifierService' diff --git a/packages/openid4vc-verifier/src/utils.ts b/packages/openid4vc/src/openid4vc-verifier/utils.ts similarity index 100% rename from packages/openid4vc-verifier/src/utils.ts rename to packages/openid4vc/src/openid4vc-verifier/utils.ts diff --git a/packages/openid4vc/tests/setup.ts b/packages/openid4vc/tests/setup.ts new file mode 100644 index 0000000000..34e38c9705 --- /dev/null +++ b/packages/openid4vc/tests/setup.ts @@ -0,0 +1 @@ +jest.setTimeout(120000) diff --git a/packages/openid4vc-holder/tsconfig.build.json b/packages/openid4vc/tsconfig.build.json similarity index 100% rename from packages/openid4vc-holder/tsconfig.build.json rename to packages/openid4vc/tsconfig.build.json diff --git a/packages/openid4vc-holder/tsconfig.json b/packages/openid4vc/tsconfig.json similarity index 100% rename from packages/openid4vc-holder/tsconfig.json rename to packages/openid4vc/tsconfig.json diff --git a/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts b/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts index b31faadbe8..af262a3e73 100644 --- a/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts +++ b/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts @@ -251,7 +251,7 @@ describe('SdJwtVcService', () => { test('Receive sd-jwt-vc from a basic payload without disclosures', async () => { const sdJwtVc = simpleJwtVc - const sdJwtVcRecord = await SdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { + const sdJwtVcRecord = await sdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { issuerDidUrl, holderDidUrl, }) @@ -278,7 +278,7 @@ describe('SdJwtVcService', () => { test('Receive sd-jwt-vc from a basic payload with a disclosure', async () => { const sdJwtVc = sdJwtVcWithSingleDisclosure - const sdJwtVcRecord = await SdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { + const sdJwtVcRecord = await sdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { issuerDidUrl, holderDidUrl, }) @@ -312,7 +312,7 @@ describe('SdJwtVcService', () => { test('Receive sd-jwt-vc from a basic payload with multiple (nested) disclosure', async () => { const sdJwtVc = complexSdJwtVc - const sdJwtVcRecord = await SdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { + const sdJwtVcRecord = await sdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { issuerDidUrl, holderDidUrl, }) @@ -382,7 +382,7 @@ describe('SdJwtVcService', () => { test('Present sd-jwt-vc from a basic payload without disclosures', async () => { const sdJwtVc = simpleJwtVc - const sdJwtVcRecord = await SdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { + const sdJwtVcRecord = await sdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { issuerDidUrl, holderDidUrl, }) @@ -403,7 +403,7 @@ describe('SdJwtVcService', () => { test('Present sd-jwt-vc from a basic payload with a disclosure', async () => { const sdJwtVc = sdJwtVcWithSingleDisclosure - const sdJwtVcRecord = await SdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { + const sdJwtVcRecord = await sdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { issuerDidUrl, holderDidUrl, }) @@ -425,7 +425,7 @@ describe('SdJwtVcService', () => { test('Present sd-jwt-vc from a basic payload with multiple (nested) disclosure', async () => { const sdJwtVc = complexSdJwtVc - const sdJwtVcRecord = await SdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { + const sdJwtVcRecord = await sdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { issuerDidUrl, holderDidUrl, }) @@ -449,7 +449,7 @@ describe('SdJwtVcService', () => { test('Verify sd-jwt-vc without disclosures', async () => { const sdJwtVc = simpleJwtVc - const sdJwtVcRecord = await SdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { + const sdJwtVcRecord = await sdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { issuerDidUrl, holderDidUrl, }) @@ -482,7 +482,7 @@ describe('SdJwtVcService', () => { test('Verify sd-jwt-vc with a disclosure', async () => { const sdJwtVc = sdJwtVcWithSingleDisclosure - const sdJwtVcRecord = await SdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { + const sdJwtVcRecord = await sdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { issuerDidUrl, holderDidUrl, }) @@ -516,7 +516,7 @@ describe('SdJwtVcService', () => { test('Verify sd-jwt-vc with multiple (nested) disclosure', async () => { const sdJwtVc = complexSdJwtVc - const sdJwtVcRecord = await SdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { + const sdJwtVcRecord = await sdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { issuerDidUrl, holderDidUrl, }) diff --git a/packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts b/packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts index 558cfe5acf..d8c7bd086f 100644 --- a/packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts +++ b/packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts @@ -14,7 +14,7 @@ import { import { agentDependencies } from '@aries-framework/node' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' -import { SdJwtVcModule, SdJwtVcService } from '../src' +import { SdJwtVcModule } from '../src' const getAgent = (label: string) => new Agent({ @@ -104,7 +104,7 @@ describe('sd-jwt-vc end to end test', () => { }, }) - const sdJwtVcRecord = await SdJwtVcService.fromSerializedJwt(issuer.context, compact, { + const sdJwtVcRecord = await holder.modules.sdJwt.fromSerializedJwt(compact, { issuerDidUrl, holderDidUrl, }) diff --git a/yarn.lock b/yarn.lock index df639f93c7..5ca24738fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2602,7 +2602,7 @@ "@sphereon/oid4vci-client@file:../Sphereon/sphereon-oidvci-client-0.8.1": version "0.8.1" dependencies: - "@sphereon/oid4vci-common" "file:../../../Library/Caches/Yarn/v6/npm-@sphereon-oid4vci-client-0.8.1-f811adc0-1b57-408f-94f9-ae255119e9b1-1701698647517/node_modules/@sphereon/sphereon-oid4vci-common-0.8.1" + "@sphereon/oid4vci-common" "file:../../../Library/Caches/Yarn/v6/npm-@sphereon-oid4vci-client-0.8.1-ac00cd04-8774-4637-b885-eccc4730bff5-1701786850804/node_modules/@sphereon/sphereon-oid4vci-common-0.8.1" "@sphereon/ssi-types" "0.17.2" cross-fetch "^3.1.8" debug "^4.3.4" @@ -2617,8 +2617,8 @@ "@sphereon/oid4vci-issuer-server@file:../Sphereon/sphereon-oid4vci-issuer-server-0.8.1": version "0.8.1" dependencies: - "@sphereon/oid4vci-common" "file:../../../Library/Caches/Yarn/v6/npm-@sphereon-oid4vci-issuer-server-0.8.1-e621511a-80af-4ac9-9a46-a4bb6eb08dee-1701698647506/node_modules/@sphereon/sphereon-oid4vci-common-0.8.1" - "@sphereon/oid4vci-issuer" "file:../../../Library/Caches/Yarn/v6/npm-@sphereon-oid4vci-issuer-server-0.8.1-e621511a-80af-4ac9-9a46-a4bb6eb08dee-1701698647506/node_modules/@sphereon/sphereon-oid4vci-issuer-0.8.1" + "@sphereon/oid4vci-common" "file:../../../Library/Caches/Yarn/v6/npm-@sphereon-oid4vci-issuer-server-0.8.1-f2353e87-1560-4059-8ec7-6727944a622f-1701786850800/node_modules/@sphereon/sphereon-oid4vci-common-0.8.1" + "@sphereon/oid4vci-issuer" "file:../../../Library/Caches/Yarn/v6/npm-@sphereon-oid4vci-issuer-server-0.8.1-f2353e87-1560-4059-8ec7-6727944a622f-1701786850800/node_modules/@sphereon/sphereon-oid4vci-issuer-0.8.1" "@sphereon/ssi-express-support" "0.17.2" "@sphereon/ssi-types" "0.17.2" body-parser "^1.20.2" @@ -2953,14 +2953,6 @@ "@types/connect" "*" "@types/node" "*" -"@types/body-parser@^1.19.5": - version "1.19.5" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" - integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== - dependencies: - "@types/connect" "*" - "@types/node" "*" - "@types/connect@*": version "3.4.35" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" @@ -8427,7 +8419,7 @@ jsonparse@^1.2.0, jsonparse@^1.3.1: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== -jsonpath@1.1.1: +jsonpath@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/jsonpath/-/jsonpath-1.1.1.tgz#0ca1ed8fb65bb3309248cc9d5466d12d5b0b9901" integrity sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w== From 8a79da1221b8eaabd362ea510d1438b546cff2aa Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Fri, 8 Dec 2023 14:18:07 +0100 Subject: [PATCH 081/115] feat: multitenant support and refactorings --- demo-openid/src/Issuer.ts | 26 +- demo-openid/src/Verifier.ts | 12 +- packages/openid4vc/package.json | 3 +- .../openid4vc-holder/OpenId4VcHolderApi.ts | 6 +- .../openid4vc/src/openid4vc-holder/README.md | 177 ------- .../openid4vc-holder/__tests__/fixtures_vp.ts | 5 - .../__tests__/openid4vci-holder.e2e.test.ts | 37 +- .../__tests__/openid4vp-holder.e2e.test.ts | 334 ++++--------- .../src/openid4vc-holder/__tests__/setup.ts | 1 - .../presentation/OpenId4VpHolderService.ts | 42 +- .../OpenId4VpHolderServiceOptions.ts | 13 +- .../PresentationExchangeService.ts | 23 +- .../selection/PexCredentialSelection.ts | 4 +- .../reception/OpenId4VciHolderService.ts | 27 +- .../openid4vc-issuer/OpenId4VcIssuerApi.ts | 16 +- .../OpenId4VcIssuerModuleConfig.ts | 76 ++- .../OpenId4VcIssuerService.ts | 424 ++++++++-------- .../OpenId4VcIssuerServiceOptions.ts | 30 +- .../openid4vc/src/openid4vc-issuer/README.md | 68 --- .../__tests__/openId4vc-issuer-module.test.ts | 9 +- .../__tests__/openid4vc-issuer.e2e.test.ts | 85 ++-- .../src/openid4vc-issuer/__tests__/setup.ts | 1 - .../router/OpenId4VcIEndpointConfiguration.ts | 135 ++--- .../router/accessTokenEndpoint.ts | 48 +- .../router/metadataEndpoint.ts | 25 + .../src/openid4vc-issuer/router/utils.ts | 34 -- .../InMemoryVerifierSessionManager.ts | 40 +- .../OpenId4VcVerifierApi.ts | 16 +- .../OpenId4VcVerifierModule.ts | 9 +- .../OpenId4VcVerifierModuleConfig.ts | 50 +- .../OpenId4VcVerifierService.ts | 201 ++++---- .../OpenId4VcVerifierServiceOptions.ts | 18 +- .../src/openid4vc-verifier/README.md | 68 --- .../openId4vc-verifier-module.test.ts | 7 +- .../__tests__/openid4vc-verifier.e2e.test.ts | 123 ++--- .../src/openid4vc-verifier/__tests__/setup.ts | 1 - .../router/OpenId4VpEndpointConfiguration.ts | 47 ++ packages/openid4vc/src/shared/router.ts | 59 +++ .../presentation => shared}/transform.ts | 17 +- .../{openid4vc-verifier => shared}/utils.ts | 22 + .../openid4vc/tests/openid4vc.e2e.test.ts | 465 ++++++++++++++++++ packages/openid4vc/tests/utils.ts | 71 +++ packages/openid4vc/tests/utilsVci.ts | 46 ++ packages/openid4vc/tests/utilsVp.ts | 88 ++++ packages/sd-jwt-vc/src/SdJwtCredential.ts | 29 ++ packages/sd-jwt-vc/src/SdJwtVcApi.ts | 7 + packages/sd-jwt-vc/src/SdJwtVcService.ts | 73 +++ packages/sd-jwt-vc/src/index.ts | 1 + yarn.lock | 8 +- 49 files changed, 1793 insertions(+), 1334 deletions(-) delete mode 100644 packages/openid4vc/src/openid4vc-holder/README.md delete mode 100644 packages/openid4vc/src/openid4vc-holder/__tests__/fixtures_vp.ts delete mode 100644 packages/openid4vc/src/openid4vc-holder/__tests__/setup.ts delete mode 100644 packages/openid4vc/src/openid4vc-issuer/README.md delete mode 100644 packages/openid4vc/src/openid4vc-issuer/__tests__/setup.ts create mode 100644 packages/openid4vc/src/openid4vc-issuer/router/metadataEndpoint.ts delete mode 100644 packages/openid4vc/src/openid4vc-issuer/router/utils.ts delete mode 100644 packages/openid4vc/src/openid4vc-verifier/README.md delete mode 100644 packages/openid4vc/src/openid4vc-verifier/__tests__/setup.ts create mode 100644 packages/openid4vc/src/openid4vc-verifier/router/OpenId4VpEndpointConfiguration.ts create mode 100644 packages/openid4vc/src/shared/router.ts rename packages/openid4vc/src/{openid4vc-holder/presentation => shared}/transform.ts (76%) rename packages/openid4vc/src/{openid4vc-verifier => shared}/utils.ts (79%) create mode 100644 packages/openid4vc/tests/openid4vc.e2e.test.ts create mode 100644 packages/openid4vc/tests/utils.ts create mode 100644 packages/openid4vc/tests/utilsVci.ts create mode 100644 packages/openid4vc/tests/utilsVp.ts create mode 100644 packages/sd-jwt-vc/src/SdJwtCredential.ts diff --git a/demo-openid/src/Issuer.ts b/demo-openid/src/Issuer.ts index 90a2cd3a05..064ca96b2b 100644 --- a/demo-openid/src/Issuer.ts +++ b/demo-openid/src/Issuer.ts @@ -9,7 +9,7 @@ import type e from 'express' import { AskarModule } from '@aries-framework/askar' import { W3cCredential, W3cCredentialSubject, W3cIssuer, w3cDate } from '@aries-framework/core' import { OpenId4VcIssuerModule, OpenIdCredentialFormatProfile } from '@aries-framework/openid4vc' -import { SdJwtVcModule } from '@aries-framework/sd-jwt-vc' +import { SdJwtCredential, SdJwtVcModule } from '@aries-framework/sd-jwt-vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { Router } from 'express' @@ -48,9 +48,9 @@ function getOpenIdIssuerModules() { askar: new AskarModule({ ariesAskar }), openId4VcIssuer: new OpenId4VcIssuerModule({ issuerMetadata: { - credentialIssuer: 'http://localhost:2000', - tokenEndpoint: 'http://localhost:2000/token', - credentialEndpoint: 'http://localhost:2000/credentials', + issuerBaseUrl: 'http://localhost:2000', + tokenEndpointPath: 'http://localhost:2000/token', + credentialEndpointPath: 'http://localhost:2000/credentials', credentialsSupported, }, }), @@ -71,6 +71,7 @@ export class Issuer extends BaseAgent> public async configureRouter(): Promise { const endpointConfig: IssuerEndpointConfig = { + basePath: '/', metadataEndpointConfig: { enabled: true }, accessTokenEndpointConfig: { enabled: true, @@ -90,7 +91,7 @@ export class Issuer extends BaseAgent> } public getCredentialRequestToCredentialMapper(): CredentialRequestToCredentialMapper { - return async (credentialRequest, { holderDid, holderDidUrl }) => { + return async ({ credentialRequest, holderDid, holderDidUrl }) => { if ( credentialRequest.format === 'jwt_vc_json' && credentialRequest.types.includes('UniversityDegreeCredential') @@ -116,15 +117,12 @@ export class Issuer extends BaseAgent> credentialRequest.format === 'vc+sd-jwt' && credentialRequest.credential_definition.vct === 'UniversityDegreeCredential' ) { - const { compact } = await this.agent.modules.sdJwtVc.create( - { type: 'UniversityDegreeCredential', university: 'innsbruck', degree: 'bachelor' }, - { - holderDidUrl, - issuerDidUrl: this.kid, - disclosureFrame: { university: true, degree: true }, - } - ) - return compact + return new SdJwtCredential({ + payload: { type: 'UniversityDegreeCredential', university: 'innsbruck', degree: 'bachelor' }, + holderDidUrl, + issuerDidUrl: this.kid, + disclosureFrame: { university: true, degree: true }, + }) } throw new Error('Invalid request') diff --git a/demo-openid/src/Verifier.ts b/demo-openid/src/Verifier.ts index 3436f98001..85b758ddcf 100644 --- a/demo-openid/src/Verifier.ts +++ b/demo-openid/src/Verifier.ts @@ -54,13 +54,16 @@ export const presentationDefinitions = [ function getOpenIdVerifierModules() { return { askar: new AskarModule({ ariesAskar }), - openId4VcVerifier: new OpenId4VcVerifierModule({}), + openId4VcVerifier: new OpenId4VcVerifierModule({ + verifierMetadata: { + verifierBaseUrl: 'http://localhost:4000', + verificationEndpointPath: '/verify', + }, + }), } as const } export class Verifier extends BaseAgent> { - private static verificationEndpointPath = '/verify' - public constructor(port: number, name: string) { super({ port, name, modules: getOpenIdVerifierModules() }) } @@ -74,9 +77,9 @@ export class Verifier extends BaseAgent { const endpointConfig: VerifierEndpointConfig = { + basePath: '/', verificationEndpointConfig: { enabled: true, - verificationEndpointPath: Verifier.verificationEndpointPath, proofResponseHandler: Verifier.proofResponseHandler, }, } @@ -88,7 +91,6 @@ export class Verifier extends BaseAgent -
- Hyperledger Aries logo -

-

Aries Framework JavaScript Open ID Connect For Verifiable Credentials Client Module

-

- License - typescript - @aries-framework/openid4vc-holder version - -

-
- -Open ID Connect For Verifiable Credentials Holder Module for [Aries Framework JavaScript](https://github.com/hyperledger/aries-framework-javascript). - -### Installation - -Make sure you have set up the correct version of Aries Framework JavaScript according to the AFJ repository. - -```sh -yarn add @aries-framework/openid4vc-holder -``` - -### Quick start - -#### Requirements - -Before a credential can be requested, you need the issuer URI. This URI starts with `openid-initiate-issuance://` and is provided by the issuer. The issuer URI is commonly acquired by scanning a QR code. - -#### Module registration - -In order to get this module to work, we need to inject it into the agent. This makes the module's functionality accessible through the agent's `modules` api. - -```ts -import { OpenId4VcHolderModule } from '@aries-framework/openid4vc-holder' - -const agent = new Agent({ - config: { - /* config */ - }, - dependencies: agentDependencies, - modules: { - openId4VcHolder: new OpenId4VcHolderModule(), - /* other custom modules */ - }, -}) - -await agent.initialize() -``` - -How the module is injected and the agent has been initialized, you can access the module's functionality through `agent.modules.openId4VcHolder`. - -#### Preparing a DID - -In order to request a credential, you'll need to provide a DID that the issuer will use for setting the credential subject. In the following snippet we create one for the sake of the example, but this can be any DID that has a _authentication verification method_ with key type `Ed25519`. - -```ts -// first we create the DID -const did = await agent.dids.create({ - method: 'key', - options: { - keyType: KeyType.Ed25519, - }, -}) - -// next we do some assertions and extract the key identifier (kid) -if ( - !did.didState.didDocument || - !did.didState.didDocument.authentication || - did.didState.didDocument.authentication.length === 0 -) { - throw new Error("Error creating did document, or did document has no 'authentication' verificationMethods") -} - -const [verificationMethod] = did.didState.didDocument.authentication -const kid = typeof verificationMethod === 'string' ? verificationMethod : verificationMethod.id -``` - -#### Requesting the credential (Pre-Authorized) - -```ts -// To request credentials(s), you need a credential offer. -// The credential offer be provided as actual payload, -// the credential offer URL or issuance initiation URL -const credentialOffer = - 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fjff.walt.id%2Fissuer-api%2Fdefault%2Foidc%22%2C%22credentials%22%3A%5B%22VerifiableId%22%2C%20%22VerifiableDiploma%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22ABC%22%7D%7D%7D' - -// The first step is to resolve the credential offer and -// get all metadata required for the issuance of the credentials. -const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) - -// The second (optional) step is to filter out the credentials which you want to request. -const selectedCredentialsForRequest = resolvedCredentialOffer.credentialsToRequest.filter((credential) => { - return credential.format === OpenIdCredentialFormatProfile.JwtVcJson && credential.types.includes('VerifiableId') -}) - -// The third step is to accept the credential offer. -// If no credentialsToRequest are specified all offered credentials are requested. -const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( - resolvedCredentialOffer, - { - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], - proofOfPossessionVerificationMethodResolver: () => verificationMethod, - verifyCredentialStatus: false, - credentialsToRequest: selectedCredentialsForRequest, - } -) - -console.log(w3cCredentialRecords) -``` - -#### Requesting the credential (Authorization Code Flow) - -Requesting credentials via the Authorization Code Flow function conceptually similar, -except that there is an intermediary step involved to resolve the authorization request, and then manually get the authorization code. - -```ts -// To request credentials(s), you need a credential offer. -// The credential offer be provided as actual payload, -// the credential offer URL or issuance initiation URL -const credentialOffer = `openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.portal.walt.id%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22OpenBadgeCredential%22%5D%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fpurl.imsglobal.org%2Fspec%2Fob%2Fv3p0%2Fcontext.json%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22OpenBadgeCredential%22%5D%7D%7D%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22b0e16785-d722-42a5-a04f-4beab28e03ea%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJiMGUxNjc4NS1kNzIyLTQyYTUtYTA0Zi00YmVhYjI4ZTAzZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.ibEpHFaHFBLWyhEf4SotDQZBeh_FMrfncWapNox1Iv1kdQWQ2cLQeS1VrCyVmPsbx0tN2MAyDFG7DnAaq8MiAA%22%2C%22user_pin_required%22%3Afalse%7D%7D%7D` - -// The first step is to resolve the credential offer and -// get all metadata required for the issuance of the credentials. -const resolvedCredentialOffer = await agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) - -// The second step is the resolve the authorization request. -const resolvedAuthorizationRequest = await agent.modules.openId4VcHolder.resolveAuthorizationRequest(resolved, { - clientId: 'test-client', - redirectUri: 'http://blank', - scope: ['openid', 'OpenBadgeCredential'], -}) - -// The resolved authorization request contains the authorizationRequestUri, -// which can be used to obtain the actual authorization code. -// Currently, this needs to be done manually -const code = - 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJiMGUxNjc4NS1kNzIyLTQyYTUtYTA0Zi00YmVhYjI4ZTAzZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.ibEpHFaHFBLWyhEf4SotDQZBeh_FMrfncWapNox1Iv1kdQWQ2cLQeS1VrCyVmPsbx0tN2MAyDFG7DnAaq8MiAA' - -// The third (optional) step is to filter out the credentials which you want to request. -const selectedCredentialsForRequest = resolvedCredentialOffer.credentialsToRequest.filter((credential) => { - return credential.format === OpenIdCredentialFormatProfile.JwtVcJson && credential.types.includes('VerifiableId') -}) - -// The fourth step is to accept the credential offer. -// If no credentialsToRequest are specified all offered credentials are requested. -const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( - resolvedCredentialOffer, - resolvedAuthorizationRequest, - code, - { - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], - proofOfPossessionVerificationMethodResolver: () => verificationMethod, - verifyCredentialStatus: false, - credentialsToRequest: selectedCredentialsForRequest, - } -) - -console.log(w3cCredentialRecords) -``` diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/fixtures_vp.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/fixtures_vp.ts deleted file mode 100644 index 48c63f4d32..0000000000 --- a/packages/openid4vc/src/openid4vc-holder/__tests__/fixtures_vp.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const waltPortalOpenBadgeJwt = - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa3RpUVFFcW0yeWFwWEJEdDFXRVZCM2RxZ3Z5emk5NkZ1RkFOWW1yZ1RyS1Y5I3o2TWt0aVFRRXFtMnlhcFhCRHQxV0VWQjNkcWd2eXppOTZGdUZBTlltcmdUcktWOSJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiT3BlbkJhZGdlQ3JlZGVudGlhbCJdLCJjcmVkZW50aWFsU3ViamVjdCI6e319LCJpc3MiOiJkaWQ6a2V5Ono2TWt0aVFRRXFtMnlhcFhCRHQxV0VWQjNkcWd2eXppOTZGdUZBTlltcmdUcktWOSIsInN1YiI6ImRpZDprZXk6ejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjIiwibmJmIjoxNzAwNzQzMzM1fQ.OcKPyaWeVV-78BWr8N4h2Cyvjtc9jzknAqvTA77hTbKCNCEbhGboo-S6yXHLC-3NWYQ1vVcqZmdPlIOrHZ7MDw' - -export const waltUniversityDegreeJwt = - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa3RpUVFFcW0yeWFwWEJEdDFXRVZCM2RxZ3Z5emk5NkZ1RkFOWW1yZ1RyS1Y5I3o2TWt0aVFRRXFtMnlhcFhCRHQxV0VWQjNkcWd2eXppOTZGdUZBTlltcmdUcktWOSJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVW5pdmVyc2l0eURlZ3JlZUNyZWRlbnRpYWwiXSwiY3JlZGVudGlhbFN1YmplY3QiOnt9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdGlRUUVxbTJ5YXBYQkR0MVdFVkIzZHFndnl6aTk2RnVGQU5ZbXJnVHJLVjkiLCJzdWIiOiJkaWQ6a2V5Ono2TWtwR1I0Z3M0UmMzWnBoNHZqOHdSbmpuQXhnQVBTeGNSOE1BVkt1dFdzcFF6YyIsIm5iZiI6MTcwMDc0MzM5NH0.EhMnE349oOvzbu0rFl-m_7FOoRsB5VucLV5tUUIW0jPxkJ7J0qVLOJTXVX4KNv_N9oeP8pgTUvydd6nxB_0KCQ' diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts index e811fee5aa..0f31c93fdb 100644 --- a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts @@ -17,7 +17,7 @@ import { w3cDate, } from '@aries-framework/core' import { agentDependencies } from '@aries-framework/node' -import { SdJwtVcModule } from '@aries-framework/sd-jwt-vc' +import { SdJwtCredential, SdJwtVcModule } from '@aries-framework/sd-jwt-vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import express, { Router, type Express } from 'express' import nock, { cleanAll, enableNetConnect } from 'nock' @@ -69,9 +69,9 @@ const baseCredentialRequestOptions = { } const issuerMetadata: IssuerMetadata = { - credentialIssuer, - credentialEndpoint: `${credentialIssuer}/credentials`, - tokenEndpoint: `${credentialIssuer}/token`, + issuerBaseUrl: credentialIssuer, + credentialEndpointPath: `/credentials`, + tokenEndpointPath: `/token`, credentialsSupported: [openBadgeCredential, universityDegreeCredentialLd, universityDegreeCredentialSdJwt], } @@ -614,6 +614,7 @@ describe('OpenId4VcHolder', () => { it('e2e flow with issuer endpoints requesting multiple credentials', async () => { const router = Router() await issuer.modules.openId4VcIssuer.configureRouter(router, { + basePath: '/', metadataEndpointConfig: { enabled: true }, accessTokenEndpointConfig: { enabled: true, @@ -623,17 +624,17 @@ describe('OpenId4VcHolder', () => { credentialEndpointConfig: { enabled: true, verificationMethod: issuerVerificationMethod, - credentialRequestToCredentialMapper: async (credentialRequest, metadata) => { + credentialRequestToCredentialMapper: async ({ credentialRequest, holderDid }) => { if ( credentialRequest.format === 'jwt_vc_json' && credentialRequest.types.includes('OpenBadgeCredential') ) { - if (metadata.holderDid !== holderDid) throw new Error('Invalid holder did') + if (holderDid !== holderDid) throw new Error('Invalid holder did') return new W3cCredential({ type: openBadgeCredential.types, issuer: new W3cIssuer({ id: issuerDid }), - credentialSubject: new W3cCredentialSubject({ id: metadata.holderDid }), + credentialSubject: new W3cCredentialSubject({ id: holderDid }), issuanceDate: w3cDate(Date.now()), }) } @@ -711,6 +712,7 @@ describe('OpenId4VcHolder', () => { it('e2e flow with issuer endpoints requesting sdjwtvc', async () => { const router = Router() await issuer.modules.openId4VcIssuer.configureRouter(router, { + basePath: '/', metadataEndpointConfig: { enabled: true }, accessTokenEndpointConfig: { enabled: true, @@ -720,22 +722,19 @@ describe('OpenId4VcHolder', () => { credentialEndpointConfig: { enabled: true, verificationMethod: issuerVerificationMethod, - credentialRequestToCredentialMapper: async (credentialRequest, metadata) => { + credentialRequestToCredentialMapper: async ({ credentialRequest, holderDid, holderDidUrl }) => { if ( credentialRequest.format === 'vc+sd-jwt' && credentialRequest.credential_definition.vct === 'UniversityDegreeCredential' ) { - if (metadata.holderDid !== holderDid) throw new Error('Invalid holder did') - - const { compact } = await issuer.modules.sdJwtVc.create( - { type: 'UniversityDegreeCredential', university: 'innsbruck', degree: 'bachelor' }, - { - holderDidUrl: metadata.holderDidUrl, - issuerDidUrl: issuerKid, - disclosureFrame: { university: true, degree: true }, - } - ) - return compact + if (holderDid !== holderDid) throw new Error('Invalid holder did') + + return new SdJwtCredential({ + payload: { type: 'UniversityDegreeCredential', university: 'innsbruck', degree: 'bachelor' }, + holderDidUrl: holderDidUrl, + issuerDidUrl: issuerKid, + disclosureFrame: { university: true, degree: true }, + }) } throw new Error('Invalid request') }, diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts index 1dcb0c3aec..b2fc4793ce 100644 --- a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts @@ -1,189 +1,66 @@ +import type { AgentType } from '../../../tests/utils' import type { CreateProofRequestOptions } from '../../openid4vc-verifier' -import type { KeyDidCreateOptions, VerificationMethod } from '@aries-framework/core' -import type { PresentationDefinitionV2 } from '@sphereon/pex-models' import type { Express } from 'express' import type { Server } from 'http' import { AskarModule } from '@aries-framework/askar' -import { Agent, DidKey, KeyType, TypedArrayEncoder, W3cJwtVerifiableCredential } from '@aries-framework/core' -import { agentDependencies } from '@aries-framework/node' +import { W3cJwtVerifiableCredential } from '@aries-framework/core' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import express, { Router } from 'express' import nock from 'nock' import { OpenId4VcHolderModule } from '..' -import { OpenId4VcVerifierModule, SigningAlgo, staticOpOpenIdConfig } from '../../openid4vc-verifier' - -import { waltPortalOpenBadgeJwt, waltUniversityDegreeJwt } from './fixtures_vp' - -// id id%22%3A%22test%22%2C%22 -// * = %2A -// TODO: error on sphereon lib PR opened -// TODO: walt issued credentials verification fails due to some time issue || //throw new Error(`Inconsistent issuance dates between JWT claim (${nbfDateAsStr}) and VC value (${issuanceDate})`); -// TODO: error walt no id in presentation definition -// TODO: error walt vc.type is an array not a string thus the filter does not work $.type (should be array according to vc data 1.1) -// TODO: jwt_vc vs jwt_vc_json - -const universityDegreePresentationDefinition: PresentationDefinitionV2 = { - id: 'UniversityDegreeCredential', - input_descriptors: [ - { - id: 'UniversityDegree', - // changed jwt_vc_json to jwt_vc - format: { jwt_vc: { alg: ['EdDSA'] } }, - // changed $.type to $.vc.type - constraints: { - fields: [{ path: ['$.vc.type.*'], filter: { type: 'string', pattern: 'UniversityDegree' } }], - }, - }, - ], -} - -const openBadgePresentationDefinition: PresentationDefinitionV2 = { - id: 'OpenBadgeCredential', - input_descriptors: [ - { - id: 'OpenBadgeCredential', - // changed jwt_vc_json to jwt_vc - format: { jwt_vc: { alg: ['EdDSA'] } }, - // changed $.type to $.vc.type - constraints: { - fields: [{ path: ['$.vc.type.*'], filter: { type: 'string', pattern: 'OpenBadgeCredential' } }], - }, - }, - ], -} - -const combinePresentationDefinitions = ( - presentationDefinitions: PresentationDefinitionV2[] -): PresentationDefinitionV2 => { - return { - id: 'Combined', - input_descriptors: presentationDefinitions.flatMap((p) => p.input_descriptors), - } -} - -const staticOpOpenIdConfigEdDSA = { - ...staticOpOpenIdConfig, - idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA], - requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA], - vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] }, jwt_vp: { alg: [SigningAlgo.EDDSA] } }, -} +import { createAgentFromModules } from '../../../tests/utils' +import { + combinePresentationDefinitions, + openBadgePresentationDefinition, + staticOpOpenIdConfigEdDSA, + universityDegreePresentationDefinition, + waitForMockFunction, + waltPortalOpenBadgeJwt, + waltUniversityDegreeJwt, +} from '../../../tests/utilsVp' +import { OpenId4VcVerifierModule } from '../../openid4vc-verifier' const port = 3121 const verificationEndpointPath = '/proofResponse' -const verificationEndpoint = `http://localhost:${port}${verificationEndpointPath}` - -const createHolderModules = () => { - const modules = { - openId4VcHolder: new OpenId4VcHolderModule(), - askar: new AskarModule({ ariesAskar }), - } +const verifierBaseUrl = `http://localhost:${port}` - return modules +const holderModules = { + openId4VcHolder: new OpenId4VcHolderModule(), + askar: new AskarModule({ ariesAskar }), } -const createVerifierModules = () => { - const modules = { - openId4VcVerifier: new OpenId4VcVerifierModule({}), - - askar: new AskarModule({ ariesAskar }), - } +const verifierModules = { + openId4VcVerifier: new OpenId4VcVerifierModule({ + verifierMetadata: { + verifierBaseUrl: verifierBaseUrl, + verificationEndpointPath, + }, + }), - return modules + askar: new AskarModule({ ariesAskar }), } -type VerifierModules = ReturnType -type HolderModules = ReturnType - describe('OpenId4VcHolder | OpenID4VP', () => { - let verifier: Agent - let verifierVerificationMethod: VerificationMethod + let verifier: AgentType + let holder: AgentType let verifierApp: Express // eslint-disable-next-line @typescript-eslint/no-explicit-any let verifierServer: Server - let holder: Agent - let holderVerificationMethod: VerificationMethod - const mockFunction = jest.fn() mockFunction.mockReturnValue({ status: 200 }) - function waitForMockFunction() { - return new Promise((resolve, reject) => { - const intervalId = setInterval(() => { - if (mockFunction.mock.calls.length > 0) { - clearInterval(intervalId) - resolve(0) - } - }, 100) - - setTimeout(() => { - clearInterval(intervalId) - reject(new Error('Timeout Callback')) - }, 10000) - }) - } - beforeEach(async () => { + verifier = await createAgentFromModules('verifier', verifierModules, '96213c3d7fc8d4d6754c7a0fd969598f') + holder = await createAgentFromModules('holder', holderModules, '96213c3d7fc8d4d6754c7a0fd969598e') verifierApp = express() - verifier = new Agent({ - config: { - label: 'OpenId4VcRp OpenID4VP Test43', - walletConfig: { - id: 'openid4vc-rp-openid4vp-test43', - key: 'openid4vc-rp-openid4vp-test43', - }, - }, - dependencies: agentDependencies, - modules: createVerifierModules(), - }) - holder = new Agent({ - config: { - label: 'OpenId4VcOp OpenID4VP Test43', - walletConfig: { - id: 'openid4vc-op-openid4vp-test43', - key: 'openid4vc-op-openid4vp-test43', - }, - }, - dependencies: agentDependencies, - modules: createHolderModules(), - }) - - await verifier.initialize() - await holder.initialize() - - const verifierDid = await verifier.dids.create({ - method: 'key', - options: { keyType: KeyType.Ed25519 }, - secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598f') }, - }) - - const verifierDidKey = DidKey.fromDid(verifierDid.didState.did as string) - const verifierKid = `${verifierDid.didState.did as string}#${verifierDidKey.key.fingerprint}` - const _verifierVerificationMethod = verifierDid.didState.didDocument?.dereferenceKey(verifierKid, [ - 'authentication', - ]) - if (!_verifierVerificationMethod) throw new Error('No verification method found') - verifierVerificationMethod = _verifierVerificationMethod - - const holderDid = await holder.dids.create({ - method: 'key', - options: { keyType: KeyType.Ed25519 }, - secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, - }) - - const holderDidKey = DidKey.fromDid(holderDid.didState.did as string) - const holderKid = `${holderDid.didState.did as string}#${holderDidKey.key.fingerprint}` - const _holderVerificationMethod = holderDid.didState.didDocument?.dereferenceKey(holderKid, ['authentication']) - if (!_holderVerificationMethod) throw new Error('No verification method found') - holderVerificationMethod = _holderVerificationMethod - - const router = await verifier.modules.openId4VcVerifier.configureRouter(Router(), { + const router = await verifier.agent.modules.openId4VcVerifier.configureRouter(Router(), { + basePath: '/', verificationEndpointConfig: { enabled: true, - verificationEndpointPath, proofResponseHandler: mockFunction, }, }) @@ -195,45 +72,46 @@ describe('OpenId4VcHolder | OpenID4VP', () => { afterEach(async () => { verifierServer?.close() - await holder.shutdown() - await holder.wallet.delete() - await verifier.shutdown() - await verifier.wallet.delete() + await holder.agent.shutdown() + await holder.agent.wallet.delete() + await verifier.agent.shutdown() + await verifier.agent.wallet.delete() }) it('siop request with static metadata', async () => { const createProofRequestOptions: CreateProofRequestOptions = { - verificationMethod: verifierVerificationMethod, - redirectUri: verificationEndpoint, + verificationMethod: verifier.verificationMethod, holderMetadata: staticOpOpenIdConfigEdDSA, } //////////////////////////// RP (create request) //////////////////////////// - const { proofRequest, proofRequestMetadata } = await verifier.modules.openId4VcVerifier.createProofRequest( + const { proofRequest, proofRequestMetadata } = await verifier.agent.modules.openId4VcVerifier.createProofRequest( createProofRequestOptions ) //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.modules.openId4VcHolder.resolveProofRequest(proofRequest) + const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(proofRequest) //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// if (result.proofType == 'presentation') throw new Error('Expected an authenticationRequest') //////////////////////////// OP (accept the verified request) //////////////////////////// - const { submittedResponse, status } = await holder.modules.openId4VcHolder.acceptAuthenticationRequest( + const { submittedResponse, status } = await holder.agent.modules.openId4VcHolder.acceptAuthenticationRequest( result.authenticationRequest, - holderVerificationMethod + holder.verificationMethod ) expect(status).toBe(200) - expect(result.authenticationRequest.authorizationRequestPayload.redirect_uri).toBe(verificationEndpoint) - expect(result.authenticationRequest.issuer).toBe(verifierVerificationMethod.controller) + expect(result.authenticationRequest.authorizationRequestPayload.redirect_uri).toBe( + verifierBaseUrl + verificationEndpointPath + ) + expect(result.authenticationRequest.issuer).toBe(verifier.verificationMethod.controller) //////////////////////////// RP (verify the response) //////////////////////////// - const { idTokenPayload, submission } = await verifier.modules.openId4VcVerifier.verifyProofResponse( + const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyProofResponse( submittedResponse ) @@ -243,7 +121,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { expect(idTokenPayload.state).toMatch(state) expect(idTokenPayload.nonce).toMatch(challenge) - await waitForMockFunction() + await waitForMockFunction(mockFunction) expect(mockFunction).toBeCalledWith({ idTokenPayload: expect.objectContaining(idTokenPayload), submission: undefined, @@ -263,35 +141,34 @@ describe('OpenId4VcHolder | OpenID4VP', () => { .reply(200, staticOpOpenIdConfigEdDSA) const createProofRequestOptions: CreateProofRequestOptions = { - verificationMethod: verifierVerificationMethod, - redirectUri: verificationEndpoint, + verificationMethod: verifier.verificationMethod, // TODO: if provided this way client metadata is not resolved for the verification method - holderIdentifier: 'https://helloworld.com', + holderMetadata: 'https://helloworld.com', } //////////////////////////// RP (create request) //////////////////////////// - const { proofRequest, proofRequestMetadata } = await verifier.modules.openId4VcVerifier.createProofRequest( + const { proofRequest, proofRequestMetadata } = await verifier.agent.modules.openId4VcVerifier.createProofRequest( createProofRequestOptions ) //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.modules.openId4VcHolder.resolveProofRequest(proofRequest) + const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(proofRequest) //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// if (result.proofType == 'presentation') throw new Error('Expected a proofType') //////////////////////////// OP (accept the verified request) //////////////////////////// - const { submittedResponse, status } = await holder.modules.openId4VcHolder.acceptAuthenticationRequest( + const { submittedResponse, status } = await holder.agent.modules.openId4VcHolder.acceptAuthenticationRequest( result.authenticationRequest, - holderVerificationMethod + holder.verificationMethod ) expect(status).toBe(200) //////////////////////////// RP (verify the response) //////////////////////////// - const { idTokenPayload, submission } = await verifier.modules.openId4VcVerifier.verifyProofResponse( + const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyProofResponse( submittedResponse ) @@ -300,7 +177,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { expect(idTokenPayload.state).toMatch(state) expect(idTokenPayload.nonce).toMatch(challenge) - await waitForMockFunction() + await waitForMockFunction(mockFunction) expect(mockFunction).toBeCalledWith({ idTokenPayload: expect.objectContaining(idTokenPayload), submission: expect.objectContaining(submission), @@ -309,16 +186,17 @@ describe('OpenId4VcHolder | OpenID4VP', () => { it('resolving vp request with no credentials', async () => { const createProofRequestOptions: CreateProofRequestOptions = { - verificationMethod: verifierVerificationMethod, - redirectUri: verificationEndpoint, + verificationMethod: verifier.verificationMethod, holderMetadata: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, } - const { proofRequest } = await verifier.modules.openId4VcVerifier.createProofRequest(createProofRequestOptions) + const { proofRequest } = await verifier.agent.modules.openId4VcVerifier.createProofRequest( + createProofRequestOptions + ) //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.modules.openId4VcHolder.resolveProofRequest(proofRequest) + const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(proofRequest) if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') expect(result.presentationSubmission.areRequirementsSatisfied).toBeFalsy() @@ -326,20 +204,21 @@ describe('OpenId4VcHolder | OpenID4VP', () => { }) it('resolving vp request with wrong credentials errors', async () => { - await holder.w3cCredentials.storeCredential({ + await holder.agent.w3cCredentials.storeCredential({ credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), }) const createProofRequestOptions: CreateProofRequestOptions = { - verificationMethod: verifierVerificationMethod, - redirectUri: verificationEndpoint, + verificationMethod: verifier.verificationMethod, holderMetadata: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, } - const { proofRequest } = await verifier.modules.openId4VcVerifier.createProofRequest(createProofRequestOptions) + const { proofRequest } = await verifier.agent.modules.openId4VcVerifier.createProofRequest( + createProofRequestOptions + ) - const result = await holder.modules.openId4VcHolder.resolveProofRequest(proofRequest) + const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(proofRequest) if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') //////////////////////////// OP (validate and parse the request) //////////////////////////// @@ -348,38 +227,37 @@ describe('OpenId4VcHolder | OpenID4VP', () => { }) it('expect submitting a wrong submission to fail', async () => { - await holder.w3cCredentials.storeCredential({ + await holder.agent.w3cCredentials.storeCredential({ credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), }) - await holder.w3cCredentials.storeCredential({ + await holder.agent.w3cCredentials.storeCredential({ credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), }) const createProofRequestOptions: CreateProofRequestOptions = { - verificationMethod: verifierVerificationMethod, - redirectUri: verificationEndpoint, + verificationMethod: verifier.verificationMethod, holderMetadata: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, } - const { proofRequest: openBadge } = await verifier.modules.openId4VcVerifier.createProofRequest( + const { proofRequest: openBadge } = await verifier.agent.modules.openId4VcVerifier.createProofRequest( createProofRequestOptions ) - const { proofRequest: university } = await verifier.modules.openId4VcVerifier.createProofRequest({ + const { proofRequest: university } = await verifier.agent.modules.openId4VcVerifier.createProofRequest({ ...createProofRequestOptions, presentationDefinition: universityDegreePresentationDefinition, }) //////////////////////////// OP (validate and parse the request) //////////////////////////// - const resolvedOpenBadge = await holder.modules.openId4VcHolder.resolveProofRequest(openBadge) - const resolvedUniversityDegree = await holder.modules.openId4VcHolder.resolveProofRequest(university) + const resolvedOpenBadge = await holder.agent.modules.openId4VcHolder.resolveProofRequest(openBadge) + const resolvedUniversityDegree = await holder.agent.modules.openId4VcHolder.resolveProofRequest(university) if (resolvedOpenBadge.proofType !== 'presentation') throw new Error('expected prooftype presentation') if (resolvedUniversityDegree.proofType !== 'presentation') throw new Error('expected prooftype presentation') await expect( - holder.modules.openId4VcHolder.acceptPresentationRequest(resolvedOpenBadge.presentationRequest, { + holder.agent.modules.openId4VcHolder.acceptPresentationRequest(resolvedOpenBadge.presentationRequest, { submission: resolvedUniversityDegree.presentationSubmission, submissionEntryIndexes: [0], }) @@ -387,26 +265,27 @@ describe('OpenId4VcHolder | OpenID4VP', () => { }) it('resolving vp request with multiple credentials in wallet only allows selecting the correct ones', async () => { - await holder.w3cCredentials.storeCredential({ + await holder.agent.w3cCredentials.storeCredential({ credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), }) - await holder.w3cCredentials.storeCredential({ + await holder.agent.w3cCredentials.storeCredential({ credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), }) const createProofRequestOptions: CreateProofRequestOptions = { - verificationMethod: verifierVerificationMethod, - redirectUri: verificationEndpoint, + verificationMethod: verifier.verificationMethod, holderMetadata: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, } - const { proofRequest } = await verifier.modules.openId4VcVerifier.createProofRequest(createProofRequestOptions) + const { proofRequest } = await verifier.agent.modules.openId4VcVerifier.createProofRequest( + createProofRequestOptions + ) //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.modules.openId4VcHolder.resolveProofRequest(proofRequest) + const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(proofRequest) if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') const { presentationRequest, presentationSubmission } = result @@ -420,17 +299,16 @@ describe('OpenId4VcHolder | OpenID4VP', () => { }) it('resolving vp request with multiple credentials in wallet select the correct credentials from the wallet', async () => { - await holder.w3cCredentials.storeCredential({ + await holder.agent.w3cCredentials.storeCredential({ credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), }) - await holder.w3cCredentials.storeCredential({ + await holder.agent.w3cCredentials.storeCredential({ credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), }) const createProofRequestOptions: CreateProofRequestOptions = { - verificationMethod: verifierVerificationMethod, - redirectUri: verificationEndpoint, + verificationMethod: verifier.verificationMethod, holderMetadata: staticOpOpenIdConfigEdDSA, presentationDefinition: combinePresentationDefinitions([ openBadgePresentationDefinition, @@ -438,11 +316,13 @@ describe('OpenId4VcHolder | OpenID4VP', () => { ]), } - const { proofRequest } = await verifier.modules.openId4VcVerifier.createProofRequest(createProofRequestOptions) + const { proofRequest } = await verifier.agent.modules.openId4VcVerifier.createProofRequest( + createProofRequestOptions + ) //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.modules.openId4VcHolder.resolveProofRequest(proofRequest) + const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(proofRequest) if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') const { presentationSubmission } = result @@ -455,7 +335,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { expect(presentationSubmission.requirements[0].submissionEntry[0].inputDescriptorId).toBe('OpenBadgeCredential') expect(presentationSubmission.requirements[1].submissionEntry[0].inputDescriptorId).toBe('UniversityDegree') - const { submittedResponse, status } = await holder.modules.openId4VcHolder.acceptPresentationRequest( + const { submittedResponse, status } = await holder.agent.modules.openId4VcHolder.acceptPresentationRequest( result.presentationRequest, { submission: result.presentationSubmission, @@ -465,7 +345,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { expect(status).toBe(200) - const { idTokenPayload, submission } = await verifier.modules.openId4VcVerifier.verifyProofResponse( + const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyProofResponse( submittedResponse ) @@ -496,7 +376,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { ]) } - await waitForMockFunction() + await waitForMockFunction(mockFunction) expect(mockFunction).toBeCalledWith({ idTokenPayload: expect.objectContaining(idTokenPayload), submission: expect.objectContaining(submission), @@ -504,17 +384,16 @@ describe('OpenId4VcHolder | OpenID4VP', () => { }) it('expect accepting a proof request with only a partial set of requirements to error', async () => { - await holder.w3cCredentials.storeCredential({ + await holder.agent.w3cCredentials.storeCredential({ credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), }) - await holder.w3cCredentials.storeCredential({ + await holder.agent.w3cCredentials.storeCredential({ credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), }) const createProofRequestOptions: CreateProofRequestOptions = { - verificationMethod: verifierVerificationMethod, - redirectUri: verificationEndpoint, + verificationMethod: verifier.verificationMethod, holderMetadata: staticOpOpenIdConfigEdDSA, presentationDefinition: combinePresentationDefinitions([ openBadgePresentationDefinition, @@ -522,15 +401,17 @@ describe('OpenId4VcHolder | OpenID4VP', () => { ]), } - const { proofRequest } = await verifier.modules.openId4VcVerifier.createProofRequest(createProofRequestOptions) + const { proofRequest } = await verifier.agent.modules.openId4VcVerifier.createProofRequest( + createProofRequestOptions + ) //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.modules.openId4VcHolder.resolveProofRequest(proofRequest) + const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(proofRequest) if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') await expect( - holder.modules.openId4VcHolder.acceptPresentationRequest(result.presentationRequest, { + holder.agent.modules.openId4VcHolder.acceptPresentationRequest(result.presentationRequest, { submission: result.presentationSubmission, submissionEntryIndexes: [0], }) @@ -538,23 +419,22 @@ describe('OpenId4VcHolder | OpenID4VP', () => { }) it('expect vp request with single requested credential to succeed', async () => { - await holder.w3cCredentials.storeCredential({ + await holder.agent.w3cCredentials.storeCredential({ credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), }) const createProofRequestOptions: CreateProofRequestOptions = { - verificationMethod: verifierVerificationMethod, - redirectUri: verificationEndpoint, + verificationMethod: verifier.verificationMethod, holderMetadata: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, } - const { proofRequest, proofRequestMetadata } = await verifier.modules.openId4VcVerifier.createProofRequest( + const { proofRequest, proofRequestMetadata } = await verifier.agent.modules.openId4VcVerifier.createProofRequest( createProofRequestOptions ) //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.modules.openId4VcHolder.resolveProofRequest(proofRequest) + const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(proofRequest) if (result.proofType === 'authentication') throw new Error('Expected a proofRequest') //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// @@ -567,7 +447,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { } //////////////////////////// OP (accept the verified request) //////////////////////////// - const { submittedResponse, status } = await holder.modules.openId4VcHolder.acceptPresentationRequest( + const { submittedResponse, status } = await holder.agent.modules.openId4VcHolder.acceptPresentationRequest( result.presentationRequest, { submission: result.presentationSubmission, @@ -580,7 +460,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { // The RP MUST validate that the aud (audience) Claim contains the value of the client_id // that the RP sent in the Authorization Request as an audience. // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. - const { idTokenPayload, submission } = await verifier.modules.openId4VcVerifier.verifyProofResponse( + const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyProofResponse( submittedResponse ) @@ -595,7 +475,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { expect(submission?.presentations).toHaveLength(1) expect(submission?.presentations[0].vcs[0].credential.type).toEqual(['VerifiableCredential', 'OpenBadgeCredential']) - await waitForMockFunction() + await waitForMockFunction(mockFunction) expect(mockFunction).toBeCalledWith({ idTokenPayload: expect.objectContaining(idTokenPayload), submission: expect.objectContaining(submission), @@ -610,7 +490,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { // 'openid4vp://authorize?response_type=vp_token&client_id=https%3A%2F%2Fverifier.portal.walt.id%2Fopenid4vc%2Fverify&response_mode=direct_post&state=97509d5c-2dd2-490b-8617-577f45e3b6d0&presentation_definition=%7B%22id%22%3A%22test%22%2C%22input_descriptors%22%3A%5B%7B%22id%22%3A%22OpenBadgeCredential%22%2C%22format%22%3A%7B%22jwt_vc%22%3A%7B%22alg%22%3A%5B%22EdDSA%22%5D%7D%7D%2C%22constraints%22%3A%7B%22fields%22%3A%5B%7B%22path%22%3A%5B%22%24.vc.type.%2A%22%5D%2C%22filter%22%3A%7B%22type%22%3A%22string%22%2C%22pattern%22%3A%22OpenBadgeCredential%22%7D%7D%5D%7D%7D%5D%7D&client_id_scheme=redirect_uri&response_uri=https%3A%2F%2Fverifier.portal.walt.id%2Fopenid4vc%2Fverify%2F97509d5c-2dd2-490b-8617-577f45e3b6d0' // //////////////////////////// OP (validate and parse the request) //////////////////////////// - // const result = await holder.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) + // const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) // if (result.proofType === 'authentication') throw new Error('Expected a proofRequest') // //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// @@ -624,7 +504,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { // } // //////////////////////////// OP (accept the verified request) //////////////////////////// - // const responseStatus = await holder.modules.openId4VcHolder.acceptPresentationRequest(presentationRequest, { + // const responseStatus = await holder.agent.modules.openId4VcHolder.acceptPresentationRequest(presentationRequest, { // submission: selectResults, // submissionEntryIndexes: [0], // }) diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/setup.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/setup.ts deleted file mode 100644 index 34e38c9705..0000000000 --- a/packages/openid4vc/src/openid4vc-holder/__tests__/setup.ts +++ /dev/null @@ -1 +0,0 @@ -jest.setTimeout(120000) diff --git a/packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderService.ts b/packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderService.ts index f470ace3b3..dd67abc623 100644 --- a/packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderService.ts @@ -1,13 +1,6 @@ -import type { - AuthenticationRequest, - PresentationRequest, - ProofSubmissionResponse, - ResolvedProofRequest, -} from './OpenId4VpHolderServiceOptions' import type { PresentationSubmission } from './selection' import type { InputDescriptorToCredentials } from './selection/types' import type { AgentContext, VerificationMethod, W3cVerifiablePresentation } from '@aries-framework/core' -import type { VerifiedAuthorizationRequest } from '@sphereon/did-auth-siop' import type { W3CVerifiablePresentation } from '@sphereon/ssi-types' import { @@ -31,24 +24,17 @@ import { VerificationMode, } from '@sphereon/did-auth-siop' -import { - getResolver, - getSuppliedSignatureFromVerificationMethod, - getSupportedDidMethods, -} from '../../openid4vc-verifier/utils' +import { getResolver, getSuppliedSignatureFromVerificationMethod, getSupportedDidMethods } from '../../shared/utils' +import { + isVerifiedAuthorizationRequestWithPresentationDefinition, + type AuthenticationRequest, + type PresentationRequest, + type ProofSubmissionResponse, + type ResolvedProofRequest, +} from './OpenId4VpHolderServiceOptions' import { PresentationExchangeService } from './PresentationExchangeService' -function isVerifiedAuthorizationRequestWithPresentationDefinition( - request: VerifiedAuthorizationRequest -): request is PresentationRequest { - return ( - request.presentationDefinitions !== undefined && - request.presentationDefinitions.length === 1 && - request.presentationDefinitions?.[0]?.definition !== undefined - ) -} - @injectable() export class OpenId4VpHolderService { private logger: Logger @@ -130,11 +116,9 @@ export class OpenId4VpHolderService { ) } - const presentationDefinition = verifiedAuthorizationRequest.presentationDefinitions[0].definition - const presentationSubmission = await this.presentationExchangeService.selectCredentialsForRequest( agentContext, - presentationDefinition + verifiedAuthorizationRequest.presentationDefinitions[0].definition ) return { proofType: 'presentation', presentationRequest: verifiedAuthorizationRequest, presentationSubmission } @@ -165,12 +149,10 @@ export class OpenId4VpHolderService { } } - const suppliedSignature = await getSuppliedSignatureFromVerificationMethod(agentContext, verificationMethod) - const authorizationResponseWithCorrelationId = await openidProvider.createAuthorizationResponse( authenticationRequest, { - signature: suppliedSignature, + signature: await getSuppliedSignatureFromVerificationMethod(agentContext, verificationMethod), issuer: verificationMethod.controller, verification: { resolveOpts: { resolver: getResolver(agentContext), noUniversalResolverFallback: true }, @@ -232,12 +214,10 @@ export class OpenId4VpHolderService { const openidProvider = await this.getOpenIdProvider(agentContext, { verificationMethod }) - const suppliedSignature = await getSuppliedSignatureFromVerificationMethod(agentContext, verificationMethod) - const authorizationResponseWithCorrelationId = await openidProvider.createAuthorizationResponse( presentationRequest, { - signature: suppliedSignature, + signature: await getSuppliedSignatureFromVerificationMethod(agentContext, verificationMethod), issuer: verificationMethod.controller, // https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-aud-of-a-request-object audience: presentationRequest.authorizationRequestPayload.client_id, diff --git a/packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderServiceOptions.ts b/packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderServiceOptions.ts index 60a756f758..7a59caf08b 100644 --- a/packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderServiceOptions.ts @@ -7,13 +7,20 @@ import type { export type AuthenticationRequest = VerifiedAuthorizationRequest -/** - * SIOPv2 Authorization Request with a single v1 / v2 presentation definition - */ export type PresentationRequest = VerifiedAuthorizationRequest & { presentationDefinitions: [PresentationDefinitionWithLocation] } +export function isVerifiedAuthorizationRequestWithPresentationDefinition( + request: VerifiedAuthorizationRequest +): request is PresentationRequest { + return ( + request.presentationDefinitions !== undefined && + request.presentationDefinitions.length === 1 && + request.presentationDefinitions?.[0]?.definition !== undefined + ) +} + export type ResolvedPresentationRequest = { proofType: 'presentation' presentationRequest: PresentationRequest diff --git a/packages/openid4vc/src/openid4vc-holder/presentation/PresentationExchangeService.ts b/packages/openid4vc/src/openid4vc-holder/presentation/PresentationExchangeService.ts index 2f13983e60..942c099577 100644 --- a/packages/openid4vc/src/openid4vc-holder/presentation/PresentationExchangeService.ts +++ b/packages/openid4vc/src/openid4vc-holder/presentation/PresentationExchangeService.ts @@ -14,9 +14,9 @@ import type { VerifiablePresentationResult, } from '@sphereon/pex' import type { - PresentationDefinitionV1, - PresentationSubmission as PexPresentationSubmission, InputDescriptorV2, + PresentationSubmission as PexPresentationSubmission, + PresentationDefinitionV1, } from '@sphereon/pex-models' import type { OriginalVerifiableCredential } from '@sphereon/ssi-types' @@ -24,23 +24,24 @@ import { AriesFrameworkError, ClaimFormat, DidsApi, - getJwkFromKey, - getKeyFromVerificationMethod, - injectable, JsonTransformer, + SignatureSuiteRegistry, + W3cCredentialRepository, W3cCredentialService, W3cPresentation, - W3cCredentialRepository, - SignatureSuiteRegistry, + getJwkFromKey, + getKeyFromVerificationMethod, + injectable, } from '@aries-framework/core' import { PEVersion, PEX, PresentationSubmissionLocation } from '@sphereon/pex' -import { selectCredentialsForRequest } from './selection/PexCredentialSelection' import { - getSphereonW3cVerifiableCredential, + getSphereonOriginalVerifiableCredential, getSphereonW3cVerifiablePresentation, getW3cVerifiablePresentationInstance, -} from './transform' +} from '../../shared/transform' + +import { selectCredentialsForRequest } from './selection/PexCredentialSelection' type ProofStructure = { [subjectId: string]: { @@ -213,7 +214,7 @@ export class PresentationExchangeService { // Get all the credentials associated with the input descriptors const credentialsForSubject = Object.values(subjectInputDescriptorsToCredentials) .flatMap((credentials) => credentials) - .map(getSphereonW3cVerifiableCredential) + .map(getSphereonOriginalVerifiableCredential) const presentationDefinitionForSubject: IPresentationDefinition = { ...presentationDefinition, diff --git a/packages/openid4vc/src/openid4vc-holder/presentation/selection/PexCredentialSelection.ts b/packages/openid4vc/src/openid4vc-holder/presentation/selection/PexCredentialSelection.ts index c6ffb7d5f8..b3fb3ad5f9 100644 --- a/packages/openid4vc/src/openid4vc-holder/presentation/selection/PexCredentialSelection.ts +++ b/packages/openid4vc/src/openid4vc-holder/presentation/selection/PexCredentialSelection.ts @@ -8,14 +8,14 @@ import { PEX } from '@sphereon/pex' import { Rules } from '@sphereon/pex-models' import { default as jp } from 'jsonpath' -import { getSphereonW3cVerifiableCredential } from '../transform' +import { getSphereonOriginalVerifiableCredential } from '../../../shared/transform' export async function selectCredentialsForRequest( presentationDefinition: IPresentationDefinition, credentialRecords: W3cCredentialRecord[], holderDIDs: string[] ): Promise { - const encodedCredentials = credentialRecords.map((c) => getSphereonW3cVerifiableCredential(c.credential)) + const encodedCredentials = credentialRecords.map((c) => getSphereonOriginalVerifiableCredential(c.credential)) if (!presentationDefinition) { throw new AriesFrameworkError('Presentation Definition is required to select credentials for submission.') diff --git a/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderService.ts b/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderService.ts index b496d9e504..11c84a529f 100644 --- a/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderService.ts @@ -41,7 +41,6 @@ import { injectable, parseDid, equalsIgnoreOrder, - getJwkClassFromKeyType, getApiForModuleByName, } from '@aries-framework/core' import { @@ -60,6 +59,8 @@ import { JsonURIMode, } from '@sphereon/oid4vci-common' +import { getSupportedJwaSignatureAlgorithms } from '../../shared/utils' + import { type AuthCodeFlowOptions, type AcceptCredentialOfferOptions, @@ -81,30 +82,6 @@ import { OfferedCredentialType, } from './utils/IssuerMetadataUtils' -// TODO: duplicate -/** - * Returns the JWA Signature Algorithms that are supported by the wallet. - * - * This is an approximation based on the supported key types of the wallet. - * This is not 100% correct as a supporting a key type does not mean you support - * all the algorithms for that key type. However, this needs refactoring of the wallet - * that is planned for the 0.5.0 release. - */ -export function getSupportedJwaSignatureAlgorithms(agentContext: AgentContext): JwaSignatureAlgorithm[] { - const supportedKeyTypes = agentContext.wallet.supportedKeyTypes - - // Extract the supported JWS algs based on the key types the wallet support. - const supportedJwaSignatureAlgorithms = supportedKeyTypes - // Map the supported key types to the supported JWK class - .map(getJwkClassFromKeyType) - // Filter out the undefined values - .filter((jwkClass): jwkClass is Exclude => jwkClass !== undefined) - // Extract the supported JWA signature algorithms from the JWK class - .flatMap((jwkClass) => jwkClass.supportedSignatureAlgorithms) - - return supportedJwaSignatureAlgorithms -} - function getV8CredentialType(offeredCredentialWithMetadata: OfferedCredentialWithMetadata, version: OpenId4VCIVersion) { if (offeredCredentialWithMetadata.offerType === OfferedCredentialType.InlineCredentialOffer) { throw new AriesFrameworkError(`Inline credential offers not supported for version < 11`) diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts index fb6d72d071..045462cff9 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts @@ -34,13 +34,13 @@ export class OpenId4VcIssuerApi { * * @param offeredCredentials - The credentials to be offered. * @param options.issuerMetadata - Metadata about the issuer. - * @param options.credentialOfferUri - The URI to retrieve the credential offer if the offer is passed by reference. - * @param options.scheme - The credential offer request scheme. Default is 'https'. - * @param options.baseUri - The base URI of the credential offer request. Default is ''. + * @param options.credentialOfferUri - The URI which references the created credential offer if the offer is passed by reference. * @param options.preAuthorizedCodeFlowConfig - The configuration for the pre-authorized code flow. This or the authorizationCodeFlowConfig must be provided. * @param options.authorizationCodeFlowConfig - The configuration for the authorization code flow. This or the preAuthorizedCodeFlowConfig must be provided. + * @param options.scheme - The credential offer request scheme. Default is 'https'. + * @param options.baseUri - The base URI of the credential offer request. Default is ''. * - * @returns Object containing the payload of the credential offer and the credential offer request, which is to be sent to the wallet. + * @returns Object containing the payload of the credential offer and the credential offer request, which can be sent to the wallet. */ public async createCredentialOfferAndRequest( offeredCredentials: OfferedCredential[], @@ -54,16 +54,16 @@ export class OpenId4VcIssuerApi { } /** - * This function retrieves a credential offer from a given URI. + * This function retrieves the credential offer referenced by the given URI. * Retrieving a credential offer from a URI is possible after a credential offer was created with * @see createCredentialOfferAndRequest and the credentialOfferUri option. * * @throws if no credential offer can found for the given URI. - * @param uri - The URI for which to retrieve the credential offer. + * @param uri - The URI referencing the credential offer. * @returns The credential offer payload associated with the given URI. */ public async getCredentialOfferFromUri(uri: string): Promise { - return await this.openId4VcIssuerService.getCredentialOfferFromUri(uri) + return await this.openId4VcIssuerService.getCredentialOfferFromUri(this.agentContext, uri) } /** @@ -71,7 +71,7 @@ export class OpenId4VcIssuerApi { * * @param options.credentialRequest - The credential request, for which to create a response. * @param options.credential - The credential to be issued. - * @param options.issuerMetadata - Metadata about the issuer. + * @param options.verificationMethod - The verification method used for signing the credential. */ public async createIssueCredentialResponse(options: CreateIssueCredentialResponseOptions) { return await this.openId4VcIssuerService.createIssueCredentialResponse(this.agentContext, options) diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts index b6666ee7e5..479aca1b6c 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts @@ -1,56 +1,84 @@ import type { IssuerMetadata } from './OpenId4VcIssuerServiceOptions' -import type { CNonceState, CredentialOfferSession, IStateManager, URIState } from '@sphereon/oid4vci-common' +import type { AgentContext } from '@aries-framework/core' +import type { CNonceState, CredentialOfferSession, IStateManager, StateType, URIState } from '@sphereon/oid4vci-common' import { MemoryStates } from '@sphereon/oid4vci-issuer' +export type StateManagerFactory = () => IStateManager + export interface OpenId4VcIssuerModuleConfigOptions { issuerMetadata: IssuerMetadata - cNonceStateManager?: IStateManager - credentialOfferSessionManager?: IStateManager - uriStateManager?: IStateManager - + cNonceStateManagerFactory?: StateManagerFactory + credentialOfferSessionManagerFactory?: StateManagerFactory + uriStateManagerFactory?: StateManagerFactory cNonceExpiresIn?: number tokenExpiresIn?: number } export class OpenId4VcIssuerModuleConfig { - private _issuerMetadata: IssuerMetadata - private _cNonceStateManager: IStateManager - private _credentialOfferSessionManager: IStateManager - private _uriStateManager: IStateManager + private options: OpenId4VcIssuerModuleConfigOptions + private uriStateManagerMap: Map> + private credentialOfferSessionManagerMap: Map> + private cNonceStateManagerMap: Map> + + private basePathMap: Map private _cNonceExpiresIn: number private _tokenExpiresIn: number public constructor(options: OpenId4VcIssuerModuleConfigOptions) { - this._issuerMetadata = options.issuerMetadata - this._cNonceStateManager = options.cNonceStateManager ?? new MemoryStates() - this._credentialOfferSessionManager = options.credentialOfferSessionManager ?? new MemoryStates() - this._uriStateManager = options.uriStateManager ?? new MemoryStates() + this.basePathMap = new Map() + this.uriStateManagerMap = new Map() + this.credentialOfferSessionManagerMap = new Map() + this.cNonceStateManagerMap = new Map() this._cNonceExpiresIn = options.cNonceExpiresIn ?? 5 * 60 * 1000 // 5 minutes this._tokenExpiresIn = options.tokenExpiresIn ?? 3 * 60 * 1000 // 3 minutes + this.options = options } public get issuerMetadata(): IssuerMetadata { - return this._issuerMetadata + return this.options.issuerMetadata } - public get cNonceStateManager(): IStateManager { - return this._cNonceStateManager + public get cNonceExpiresIn(): number { + return this._cNonceExpiresIn } - public get credentialOfferSessionManager(): IStateManager { - return this._credentialOfferSessionManager + public get tokenExpiresIn(): number { + return this._tokenExpiresIn } - public get uriStateManager(): IStateManager { - return this._uriStateManager + public getBasePath(agentContext: AgentContext): string { + return this.basePathMap.get(agentContext.contextCorrelationId) ?? '/' } - public get cNonceExpiresIn(): number { - return this._cNonceExpiresIn + public setBasePath(agentContext: AgentContext, basePath: string): void { + this.basePathMap.set(agentContext.contextCorrelationId, basePath) } - public get tokenExpiresIn(): number { - return this._tokenExpiresIn + public getUriStateManager(agentContext: AgentContext) { + const val = this.uriStateManagerMap.get(agentContext.contextCorrelationId) + if (val) return val + + const newVal = this.options.uriStateManagerFactory?.() ?? new MemoryStates() + this.uriStateManagerMap.set(agentContext.contextCorrelationId, newVal) + return newVal + } + + public getCredentialOfferSessionStateManager(agentContext: AgentContext) { + const val = this.credentialOfferSessionManagerMap.get(agentContext.contextCorrelationId) + if (val) return val + + const newVal = this.options.credentialOfferSessionManagerFactory?.() ?? new MemoryStates() + this.credentialOfferSessionManagerMap.set(agentContext.contextCorrelationId, newVal) + return newVal + } + + public getCNonceStateManager(agentContext: AgentContext) { + const val = this.cNonceStateManagerMap.get(agentContext.contextCorrelationId) + if (val) return val + + const newVal = this.options.cNonceStateManagerFactory?.() ?? new MemoryStates() + this.cNonceStateManagerMap.set(agentContext.contextCorrelationId, newVal) + return newVal } } diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts index ac4787e652..6806fd702d 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts @@ -1,53 +1,56 @@ import type { - CreateCredentialOfferAndRequestOptions, AuthorizationCodeFlowConfig, - PreAuthorizedCodeFlowConfig, - OfferedCredential, + CreateCredentialOfferAndRequestOptions, CreateIssueCredentialResponseOptions, - CredentialSupported, CredentialOfferAndRequest, + CredentialSupported, IssuerEndpointConfig, + IssuerMetadata, + OfferedCredential, + PreAuthorizedCodeFlowConfig, } from './OpenId4VcIssuerServiceOptions' +import type { IssuanceRequest } from './router/OpenId4VcIEndpointConfiguration' import type { OfferedCredentialWithMetadata } from '../openid4vc-holder/reception/utils/IssuerMetadataUtils' import type { AgentContext, - VerificationMethod, - W3cVerifiableCredential, DidDocument, JwaSignatureAlgorithm, + VerificationMethod, + W3cVerifiableCredential, } from '@aries-framework/core' +import type { SdJwtCredential, SdJwtVcModule } from '@aries-framework/sd-jwt-vc' import type { + CredentialOfferPayloadV1_0_11, + CredentialRequestV1_0_11, Grant, - MetadataDisplay, JWTVerifyCallback, - CredentialRequestV1_0_11, - CredentialOfferPayloadV1_0_11, } from '@sphereon/oid4vci-common' import type { CredentialDataSupplier, CredentialDataSupplierArgs, CredentialSignerCallback, } from '@sphereon/oid4vci-issuer' -import type { ICredential, W3CVerifiableCredential as SphereonW3cVerifiableCredential } from '@sphereon/ssi-types' -import type { Router } from 'express' +import type { ICredential } from '@sphereon/ssi-types' +import type { NextFunction, Response, Router } from 'express' import { + AgentContextProvider, AriesFrameworkError, ClaimFormat, + DidsApi, InjectionSymbols, + JsonTransformer, JwsService, + Jwt, Logger, + W3cCredential, W3cCredentialService, + equalsIgnoreOrder, + getApiForModuleByName, + getJwkFromKey, + getKeyFromVerificationMethod, inject, injectable, - JsonTransformer, - W3cCredential, - Jwt, - SignatureSuiteRegistry, - DidsApi, - getKeyFromVerificationMethod, - getJwkFromKey, - equalsIgnoreOrder, } from '@aries-framework/core' import { IssueStatus } from '@sphereon/oid4vci-common' import { VcIssuerBuilder } from '@sphereon/oid4vci-issuer' @@ -55,28 +58,13 @@ import bodyParser from 'body-parser' import { OpenIdCredentialFormatProfile } from '../openid4vc-holder' import { getOfferedCredentialsWithMetadata } from '../openid4vc-holder/reception/utils/IssuerMetadataUtils' +import { getEndpointUrl, initializeAgentFromContext, getRequestContext } from '../shared/router' +import { getSphereonW3cVerifiableCredential } from '../shared/transform' +import { getProofTypeFromVerificationMethod } from '../shared/utils' import { OpenId4VcIssuerModuleConfig } from './OpenId4VcIssuerModuleConfig' -import { - configureAccessTokenEndpoint, - configureCredentialEndpoint, - configureIssuerMetadataEndpoint, -} from './router/OpenId4VcIEndpointConfiguration' - -// TODO: duplicate -function getSphereonW3cVerifiableCredential( - w3cVerifiableCredential: W3cVerifiableCredential -): SphereonW3cVerifiableCredential { - if (w3cVerifiableCredential.claimFormat === ClaimFormat.LdpVc) { - return JsonTransformer.toJSON(w3cVerifiableCredential) as SphereonW3cVerifiableCredential - } else if (w3cVerifiableCredential.claimFormat === ClaimFormat.JwtVc) { - return w3cVerifiableCredential.serializedJwt - } else { - throw new AriesFrameworkError( - `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` - ) - } -} +import { configureAccessTokenEndpoint, configureCredentialEndpoint } from './router/OpenId4VcIEndpointConfiguration' +import { configureIssuerMetadataEndpoint } from './router/metadataEndpoint' /** * @internal @@ -86,47 +74,45 @@ export class OpenId4VcIssuerService { private logger: Logger private w3cCredentialService: W3cCredentialService private jwsService: JwsService - private openId4VcIssuerModuleConfig: OpenId4VcIssuerModuleConfig + private _openId4VcIssuerModuleConfig: OpenId4VcIssuerModuleConfig + private agentContextProvider: AgentContextProvider - public get issuerMetadata() { - return this.openId4VcIssuerModuleConfig.issuerMetadata - } - - public get cNonceStateManager() { - return this.openId4VcIssuerModuleConfig.cNonceStateManager + public get openId4VcIssuerModuleConfig() { + return this._openId4VcIssuerModuleConfig } - public get credentialOfferSessionManager() { - return this.openId4VcIssuerModuleConfig.credentialOfferSessionManager - } - - public get uriStateManager() { - return this.openId4VcIssuerModuleConfig.uriStateManager + public get issuerMetadata() { + return this.openId4VcIssuerModuleConfig.issuerMetadata } public constructor( @inject(InjectionSymbols.Logger) logger: Logger, + @inject(InjectionSymbols.AgentContextProvider) agentContextProvider: AgentContextProvider, openId4VcIssuerModuleConfig: OpenId4VcIssuerModuleConfig, w3cCredentialService: W3cCredentialService, jwsService: JwsService ) { + this.agentContextProvider = agentContextProvider this.w3cCredentialService = w3cCredentialService this.logger = logger - this.openId4VcIssuerModuleConfig = openId4VcIssuerModuleConfig + this._openId4VcIssuerModuleConfig = openId4VcIssuerModuleConfig this.jwsService = jwsService } - private getProofTypeForLdpVc(agentContext: AgentContext, verificationMethod: VerificationMethod) { - const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) + public expandEndpointsWithBase(agentContext: AgentContext): IssuerMetadata { + const issuerMetadata = this.issuerMetadata + const basePath = this.openId4VcIssuerModuleConfig.getBasePath(agentContext) - const supportedSignatureSuite = signatureSuiteRegistry.getByVerificationMethodType(verificationMethod.type) - if (!supportedSignatureSuite) { - throw new AriesFrameworkError( - `Couldn't find a supported signature suite for the given verification method type '${verificationMethod.type}'.` - ) - } + const credentialIssuer = getEndpointUrl(issuerMetadata.issuerBaseUrl, basePath) + const tokenEndpoint = getEndpointUrl(credentialIssuer, basePath, issuerMetadata.tokenEndpointPath) + const credentialEndpoint = getEndpointUrl(credentialIssuer, basePath, issuerMetadata.credentialEndpointPath) - return supportedSignatureSuite.proofType + return { + ...issuerMetadata, + issuerBaseUrl: credentialIssuer, + tokenEndpointPath: tokenEndpoint, + credentialEndpointPath: credentialEndpoint, + } } private getJwtVerifyCallback = (agentContext: AgentContext): JWTVerifyCallback => { @@ -170,86 +156,37 @@ export class OpenId4VcIssuerService { } } - private getSdJwtVcCredentialSigningCallback = (): CredentialSignerCallback => { - return async (opts) => { - const { credential } = opts - // TODO: sdjwt - return credential as any - } - } - - private getW3cCredentialSigningCallback = ( - agentContext: AgentContext, - issuerVerificationMethod: VerificationMethod - ): CredentialSignerCallback => { - return async (opts) => { - const { credential, jwtVerifyResult, format } = opts - - const { alg, kid, didDocument: holderDidDocument } = jwtVerifyResult - - if (!kid) throw new AriesFrameworkError('Missing Kid. Cannot create the holder binding') - if (!holderDidDocument) throw new AriesFrameworkError('Missing did document. Cannot create the holder binding.') - - // If the Credential shall be bound to a DID, the kid refers to a DID URL which identifies a - // particular key in the DID Document that the Credential shall be bound to. - const holderVerificationMethod = holderDidDocument.dereferenceKey(kid, ['assertionMethod']) - - let signed: W3cVerifiableCredential - if (format === OpenIdCredentialFormatProfile.JwtVcJson || format === OpenIdCredentialFormatProfile.JwtVcJsonLd) { - signed = await this.w3cCredentialService.signCredential(agentContext, { - format: ClaimFormat.JwtVc, - credential: W3cCredential.fromJson(credential), - verificationMethod: issuerVerificationMethod.id, - alg: alg as JwaSignatureAlgorithm, - }) - } else if (format === OpenIdCredentialFormatProfile.LdpVc) { - signed = await this.w3cCredentialService.signCredential(agentContext, { - format: ClaimFormat.LdpVc, - credential: W3cCredential.fromJson(credential), - verificationMethod: issuerVerificationMethod.id, - proofPurpose: 'assertionMethod', - proofType: this.getProofTypeForLdpVc(agentContext, holderVerificationMethod), - }) - } else { - throw new AriesFrameworkError(`Unsupported credential format '${format}' for W3C credential signing callback.`) - } - - return getSphereonW3cVerifiableCredential(signed) - } - } + private getVcIssuer(agentContext: AgentContext) { + const issuerMetadata = this.expandEndpointsWithBase(agentContext) + const { + issuerBaseUrl: credentialIssuer, + tokenEndpointPath: tokenEndpoint, + credentialEndpointPath: credentialEndpoint, + credentialsSupported, + } = issuerMetadata - private getVcIssuer( - agentContext: AgentContext, - options: { - credentialIssuer: string - credentialEndpoint: string - tokenEndpoint: string - credentialsSupported: CredentialSupported[] - authorizationServer?: string - issuerDisplay?: MetadataDisplay | MetadataDisplay[] - } - ) { - const { credentialIssuer, tokenEndpoint, credentialEndpoint, credentialsSupported } = options const builder = new VcIssuerBuilder() - .withCredentialIssuer(credentialIssuer) - .withCredentialEndpoint(credentialEndpoint) - .withTokenEndpoint(tokenEndpoint) + .withCredentialIssuer(credentialIssuer.toString()) + .withCredentialEndpoint(credentialEndpoint.toString()) + .withTokenEndpoint(tokenEndpoint.toString()) .withCredentialsSupported(credentialsSupported) .withCNonceExpiresIn(this.openId4VcIssuerModuleConfig.cNonceExpiresIn) - .withCNonceStateManager(this.cNonceStateManager) - .withCredentialOfferStateManager(this.credentialOfferSessionManager) - .withCredentialOfferURIStateManager(this.uriStateManager) + .withCNonceStateManager(this.openId4VcIssuerModuleConfig.getCNonceStateManager(agentContext)) + .withCredentialOfferStateManager( + this.openId4VcIssuerModuleConfig.getCredentialOfferSessionStateManager(agentContext) + ) + .withCredentialOfferURIStateManager(this.openId4VcIssuerModuleConfig.getUriStateManager(agentContext)) .withJWTVerifyCallback(this.getJwtVerifyCallback(agentContext)) .withCredentialSignerCallback(() => { throw new AriesFrameworkError('this should never ba called') }) - if (options.authorizationServer) { - builder.withAuthorizationServer(options.authorizationServer) + if (issuerMetadata.authorizationServerUrl) { + builder.withAuthorizationServer(issuerMetadata.authorizationServerUrl.toString()) } - if (options.issuerDisplay) { - builder.withIssuerDisplay(options.issuerDisplay) + if (issuerMetadata.issuerDisplay) { + builder.withIssuerDisplay(issuerMetadata.issuerDisplay) } return builder.build() @@ -288,13 +225,11 @@ export class OpenId4VcIssuerService { ): Promise { const { preAuthorizedCodeFlowConfig, authorizationCodeFlowConfig } = options - const issuerMetadata = options.issuerMetadata ?? this.issuerMetadata - // this checks if the structure of the credentials is correct // it throws an error if a offered credential cannot be found in the credentialsSupported - getOfferedCredentialsWithMetadata(offeredCredentials, issuerMetadata.credentialsSupported) + getOfferedCredentialsWithMetadata(offeredCredentials, this.issuerMetadata.credentialsSupported) - const vcIssuer = this.getVcIssuer(agentContext, issuerMetadata) + const vcIssuer = this.getVcIssuer(agentContext) const { uri, session } = await vcIssuer.createCredentialOfferURI({ grants: await this.getGrantsFromConfig(agentContext, preAuthorizedCodeFlowConfig, authorizationCodeFlowConfig), @@ -311,19 +246,20 @@ export class OpenId4VcIssuerService { } } - private async getCredentialOfferSessionFromUri(uri: string) { - const uriState = await this.uriStateManager.get(uri) + private async getCredentialOfferSessionFromUri(agentContext: AgentContext, uri: string) { + const uriState = await this.openId4VcIssuerModuleConfig.getUriStateManager(agentContext).get(uri) if (!uriState) throw new AriesFrameworkError(`Credential offer uri '${uri}' not found.`) const credentialOfferSessionId = uriState.preAuthorizedCode ?? uriState.issuerState - if (!credentialOfferSessionId) { throw new AriesFrameworkError( `Credential offer uri '${uri}' is not associated with a preAuthorizedCode or issuerState.` ) } - const credentialOfferSession = await this.credentialOfferSessionManager.get(credentialOfferSessionId) + const credentialOfferSession = await this.openId4VcIssuerModuleConfig + .getCredentialOfferSessionStateManager(agentContext) + .get(credentialOfferSessionId) if (!credentialOfferSession) throw new AriesFrameworkError( `Credential offer session for '${uri}' with id '${credentialOfferSessionId}' not found.` @@ -332,12 +268,17 @@ export class OpenId4VcIssuerService { return { credentialOfferSessionId, credentialOfferSession } } - public async getCredentialOfferFromUri(uri: string) { - const { credentialOfferSession, credentialOfferSessionId } = await this.getCredentialOfferSessionFromUri(uri) + public async getCredentialOfferFromUri(agentContext: AgentContext, uri: string) { + const { credentialOfferSessionId, credentialOfferSession } = await this.getCredentialOfferSessionFromUri( + agentContext, + uri + ) credentialOfferSession.lastUpdatedAt = +new Date() credentialOfferSession.status = IssueStatus.OFFER_URI_RETRIEVED - await this.credentialOfferSessionManager.set(credentialOfferSessionId, credentialOfferSession) + await this.openId4VcIssuerModuleConfig + .getCredentialOfferSessionStateManager(agentContext) + .set(credentialOfferSessionId, credentialOfferSession) return credentialOfferSession.credentialOffer.credential_offer } @@ -365,10 +306,61 @@ export class OpenId4VcIssuerService { }) } + private getSdJwtVcCredentialSigningCallback = (agentContext: AgentContext): CredentialSignerCallback => { + return async (opts) => { + const { credential } = opts + + const sdJwtVcApi = getApiForModuleByName(agentContext, 'SdJwtVcModule') + if (!sdJwtVcApi) throw new AriesFrameworkError(`Could not find the SdJwtVcApi`) + const { compact } = await sdJwtVcApi.signCredential(credential as any) + + return compact as any + } + } + + private getW3cCredentialSigningCallback = ( + agentContext: AgentContext, + issuerVerificationMethod: VerificationMethod + ): CredentialSignerCallback => { + return async (opts) => { + const { credential, jwtVerifyResult, format } = opts + + const { alg, kid, didDocument: holderDidDocument } = jwtVerifyResult + + if (!kid) throw new AriesFrameworkError('Missing Kid. Cannot create the holder binding') + if (!holderDidDocument) throw new AriesFrameworkError('Missing did document. Cannot create the holder binding.') + + // If the Credential shall be bound to a DID, the kid refers to a DID URL which identifies a + // particular key in the DID Document that the Credential shall be bound to. + const holderVerificationMethod = holderDidDocument.dereferenceKey(kid, ['assertionMethod']) + + let signed: W3cVerifiableCredential + if (format === OpenIdCredentialFormatProfile.JwtVcJson || format === OpenIdCredentialFormatProfile.JwtVcJsonLd) { + signed = await this.w3cCredentialService.signCredential(agentContext, { + format: ClaimFormat.JwtVc, + credential: W3cCredential.fromJson(credential), + verificationMethod: issuerVerificationMethod.id, + alg: alg as JwaSignatureAlgorithm, + }) + } else if (format === OpenIdCredentialFormatProfile.LdpVc) { + signed = await this.w3cCredentialService.signCredential(agentContext, { + format: ClaimFormat.LdpVc, + credential: W3cCredential.fromJson(credential), + verificationMethod: issuerVerificationMethod.id, + proofPurpose: 'assertionMethod', + proofType: getProofTypeFromVerificationMethod(agentContext, holderVerificationMethod), + }) + } else { + throw new AriesFrameworkError(`Unsupported credential format '${format}' for W3C credential signing callback.`) + } + + return getSphereonW3cVerifiableCredential(signed) + } + } + private getCredentialDataSupplier = ( agentContext: AgentContext, - credential: string | W3cCredential, - credentialsSupported: CredentialSupported[], + credential: SdJwtCredential | W3cCredential, issuerVerificationMethod: VerificationMethod ): CredentialDataSupplier => { return async (args: CredentialDataSupplierArgs) => { @@ -377,40 +369,55 @@ export class OpenId4VcIssuerService { const offeredCredentialsMatchingRequest = this.findOfferedCredentialsMatchingRequest( credentialOffer.credential_offer, credentialRequest, - credentialsSupported + this.issuerMetadata.credentialsSupported ) if (offeredCredentialsMatchingRequest.length === 0) { - throw new AriesFrameworkError('No offered credential matches the requested credential.') + throw new AriesFrameworkError('No offered credentials match the credential request.') } - if (credentialRequest.format === OpenIdCredentialFormatProfile.SdJwtVc) { - return { - format: credentialRequest.format, - credential: credential as any, // TODO: sdjwt - signCallback: this.getSdJwtVcCredentialSigningCallback(), + if (credential instanceof W3cCredential) { + if ( + credentialRequest.format !== OpenIdCredentialFormatProfile.JwtVcJson && + credentialRequest.format !== OpenIdCredentialFormatProfile.JwtVcJsonLd && + credentialRequest.format !== OpenIdCredentialFormatProfile.LdpVc + ) { + throw new AriesFrameworkError( + `The credential to be issued does not match the request. Cannot issue a W3cCredential if the client expects a credential of format '${credentialRequest.format}'.` + ) } - } - - if (typeof credential === 'string') { - throw new AriesFrameworkError( - `Credential must be a W3C credential if not using '${OpenIdCredentialFormatProfile.SdJwtVc}' format.` - ) - } + const issuedCredentialMatchesRequest = offeredCredentialsMatchingRequest.find((offeredCredential) => { + return equalsIgnoreOrder(offeredCredential.types, credential.type) + }) - // TODO: Valide SdJwtVc Types - const issuedCredentialMatchesRequest = offeredCredentialsMatchingRequest.find((offeredCredential) => { - return equalsIgnoreOrder(offeredCredential.types, credential.type) - }) + if (!issuedCredentialMatchesRequest) { + throw new AriesFrameworkError( + `The types of the offered credentials do not match the types of the requested credential. Requested '${credential.type}'.` + ) + } - if (!issuedCredentialMatchesRequest) { - throw new AriesFrameworkError('The credential to be issued does not match the request.') - } + return { + format: credentialRequest.format, + credential: JsonTransformer.toJSON(credential) as ICredential, + signCallback: this.getW3cCredentialSigningCallback(agentContext, issuerVerificationMethod), + } + } else { + if (credentialRequest.format !== OpenIdCredentialFormatProfile.SdJwtVc) { + throw new AriesFrameworkError( + `Invalid credential format. Expected '${OpenIdCredentialFormatProfile.SdJwtVc}', received '${credentialRequest.format}'.` + ) + } + if (credentialRequest.credential_definition.vct !== credential.payload.type) { + throw new AriesFrameworkError( + `The types of the offered credentials do not match the types of the requested credential. Offered '${credential.payload.vct}' Requested '${credentialRequest.credential_definition.vct}'.` + ) + } - return { - format: credentialRequest.format, - credential: JsonTransformer.toJSON(credential) as ICredential, - signCallback: this.getW3cCredentialSigningCallback(agentContext, issuerVerificationMethod), + return { + format: credentialRequest.format, + credential: credential as any, // TODO: sdjwt + signCallback: this.getSdJwtVcCredentialSigningCallback(agentContext), + } } } } @@ -420,24 +427,14 @@ export class OpenId4VcIssuerService { options: CreateIssueCredentialResponseOptions ) { const { credentialRequest, credential, verificationMethod } = options + if (!credentialRequest.proof) throw new AriesFrameworkError('No proof defined in the credentialRequest.') - if (!credentialRequest.proof) { - throw new AriesFrameworkError('No proof defined in the credentialRequest.') - } - - const issuerMetadata = options.issuerMetadata ?? this.issuerMetadata - const vcIssuer = this.getVcIssuer(agentContext, issuerMetadata) - + const vcIssuer = this.getVcIssuer(agentContext) const issueCredentialResponse = await vcIssuer.issueCredential({ credentialRequest, tokenExpiresIn: this.openId4VcIssuerModuleConfig.tokenExpiresIn, cNonceExpiresIn: this.openId4VcIssuerModuleConfig.cNonceExpiresIn, - credentialDataSupplier: this.getCredentialDataSupplier( - agentContext, - credential, - issuerMetadata.credentialsSupported, - verificationMethod - ), + credentialDataSupplier: this.getCredentialDataSupplier(agentContext, credential, verificationMethod), credential: undefined, newCNonce: undefined, credentialDataSupplierInput: undefined, @@ -445,7 +442,7 @@ export class OpenId4VcIssuerService { }) if (!issueCredentialResponse.credential) { - throw new AriesFrameworkError('No credential defined in the issueCredentialResponse.') + throw new AriesFrameworkError('No credential found in the issueCredentialResponse.') } if (issueCredentialResponse.acceptance_token) { @@ -455,43 +452,70 @@ export class OpenId4VcIssuerService { return issueCredentialResponse } - public configureRouter = (agentContext: AgentContext, router: Router, endpointConfig: IssuerEndpointConfig) => { + public configureRouter = ( + initializationContext: AgentContext, + router: Router, + endpointConfig: IssuerEndpointConfig + ) => { + const { basePath } = endpointConfig + this.openId4VcIssuerModuleConfig.setBasePath(initializationContext, basePath) + // parse application/x-www-form-urlencoded router.use(bodyParser.urlencoded({ extended: false })) // parse application/json router.use(bodyParser.json()) + // initialize the agent and set the request context + router.use(async (req: IssuanceRequest, _res: Response, next: NextFunction) => { + const agentContext = await initializeAgentFromContext( + initializationContext.contextCorrelationId, + this.agentContextProvider + ) + + req.requestContext = { + agentContext, + openId4vcIssuerService: agentContext.dependencyManager.resolve(OpenId4VcIssuerService), + logger: agentContext.dependencyManager.resolve(InjectionSymbols.Logger), + } + + next() + }) if (endpointConfig.metadataEndpointConfig?.enabled) { - configureIssuerMetadataEndpoint(router, this.logger, { - ...endpointConfig.metadataEndpointConfig, - issuerMetadata: this.issuerMetadata, - }) + const wellKnownPath = `/.well-known/openid-credential-issuer` + configureIssuerMetadataEndpoint(router, wellKnownPath) + + const endpointPath = getEndpointUrl(this.issuerMetadata.issuerBaseUrl, basePath, wellKnownPath) + this.logger.info(`[OID4VCI] Metadata endpoint running at '${endpointPath}'.`) } if (endpointConfig.accessTokenEndpointConfig?.enabled) { - configureAccessTokenEndpoint(agentContext, router, this.logger, { + const accessTokenEndpointPath = this.issuerMetadata.tokenEndpointPath + configureAccessTokenEndpoint(router, accessTokenEndpointPath, { ...endpointConfig.accessTokenEndpointConfig, - issuerMetadata: this.issuerMetadata, cNonceExpiresIn: this.openId4VcIssuerModuleConfig.cNonceExpiresIn, tokenExpiresIn: this.openId4VcIssuerModuleConfig.tokenExpiresIn, - cNonceStateManager: this.cNonceStateManager, - credentialOfferSessionManager: this.credentialOfferSessionManager, }) + + const endpointPath = getEndpointUrl(this.issuerMetadata.issuerBaseUrl, basePath, accessTokenEndpointPath) + this.logger.info(`[OID4VCI] Token endpoint running at '${endpointPath}'.`) } if (endpointConfig.credentialEndpointConfig?.enabled) { - configureCredentialEndpoint(agentContext, router, this.logger, { + const credentialEndpointPath = this.issuerMetadata.credentialEndpointPath + configureCredentialEndpoint(router, credentialEndpointPath, { ...endpointConfig.credentialEndpointConfig, - issuerMetadata: this.issuerMetadata, - cNonceStateManager: this.cNonceStateManager, - credentialOfferSessionManager: this.credentialOfferSessionManager, - createIssueCredentialResponse: (agentContext, options) => { - const issuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService) - return issuerService.createIssueCredentialResponse(agentContext, options) - }, }) + + const endpointUrl = getEndpointUrl(this.issuerMetadata.issuerBaseUrl, basePath, credentialEndpointPath) + this.logger.info(`[OID4VCI] Credential endpoint running at '${endpointUrl}'.`) } + router.use(async (req: IssuanceRequest, _res, next) => { + const { agentContext } = getRequestContext(req) + await agentContext.endSession() + next() + }) + return router } } diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts index 918460a849..45e1320227 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts @@ -1,4 +1,5 @@ -import type { VerificationMethod, W3cCredential } from '@aries-framework/core' +import type { AgentContext, VerificationMethod, W3cCredential } from '@aries-framework/core' +import type { SdJwtCredential } from '@aries-framework/sd-jwt-vc' import type { CNonceState, CredentialOfferFormat, @@ -28,10 +29,10 @@ export type AuthorizationCodeFlowConfig = { export type IssuerMetadata = { // The Credential Issuer's identifier. (URL using the https scheme) - credentialIssuer: string - credentialEndpoint: string - tokenEndpoint: string - authorizationServer?: string + issuerBaseUrl: string + credentialEndpointPath: string + tokenEndpointPath: string + authorizationServerUrl?: string issuerDisplay?: MetadataDisplay credentialsSupported: CredentialSupported[] @@ -48,8 +49,6 @@ export interface CreateCredentialOfferAndRequestOptions { authorizationCodeFlowConfig?: AuthorizationCodeFlowConfig credentialOfferUri?: string - - issuerMetadata?: IssuerMetadata } export type CredentialOfferAndRequest = { @@ -59,9 +58,8 @@ export type CredentialOfferAndRequest = { export interface CreateIssueCredentialResponseOptions { credentialRequest: CredentialRequestV1_0_11 - credential: W3cCredential | string + credential: W3cCredential | SdJwtCredential verificationMethod: VerificationMethod - issuerMetadata?: IssuerMetadata } export { CredentialRequestV1_0_11 } @@ -70,7 +68,7 @@ export { CredentialResponse } from '@sphereon/oid4vci-common' export interface MetadataEndpointConfig { /** - * Configures the router to expose the m3tadata endpoint. + * Configures the router to expose the metadata endpoint. */ enabled: boolean } @@ -98,17 +96,14 @@ export interface AccessTokenEndpointConfig { preAuthorizedCodeExpirationDuration: number } -export type CredentialRequestToCredentialMetadata = { +export type CredentialRequestToCredentialMapper = (options: { + agentContext: AgentContext + credentialRequest: CredentialRequestV1_0_11 holderDid: string holderDidUrl: string cNonceState: CNonceState credentialOfferSession: CredentialOfferSession -} - -export type CredentialRequestToCredentialMapper = ( - credentialRequest: CredentialRequestV1_0_11, - metadata: CredentialRequestToCredentialMetadata -) => Promise +}) => Promise export interface CredentialEndpointConfig { /** @@ -128,6 +123,7 @@ export interface CredentialEndpointConfig { } export interface IssuerEndpointConfig { + basePath: string metadataEndpointConfig?: MetadataEndpointConfig accessTokenEndpointConfig?: AccessTokenEndpointConfig credentialEndpointConfig?: CredentialEndpointConfig diff --git a/packages/openid4vc/src/openid4vc-issuer/README.md b/packages/openid4vc/src/openid4vc-issuer/README.md deleted file mode 100644 index 1a62c933de..0000000000 --- a/packages/openid4vc/src/openid4vc-issuer/README.md +++ /dev/null @@ -1,68 +0,0 @@ -

-
- Hyperledger Aries logo -

-

Aries Framework JavaScript Open ID Connect For Verifiable Credentials Client Module

-

- License - typescript - @aries-framework/openid4vc-issuer version - -

-
- -Open ID Connect For Verifiable Credentials Issuer Module for [Aries Framework JavaScript](https://github.com/hyperledger/aries-framework-javascript). - -### Installation - -Make sure you have set up the correct version of Aries Framework JavaScript according to the AFJ repository. - -```sh -yarn add @aries-framework/openid4vc-issuer -``` - -### Quick start - -#### Requirements - -#### Module registration - -In order to get this module to work, we need to inject it into the agent. This makes the module's functionality accessible through the agent's `modules` api. - -```ts -import { OpenId4VcIssuerModule } from '@aries-framework/openid4vc-issuer' - -const agent = new Agent({ - config: { - /* config */ - }, - dependencies: agentDependencies, - modules: { - openId4VcIssuer: new OpenId4VcIssuerModule(), - /* other custom modules */ - }, -}) - -await agent.initialize() -``` - -How the module is injected and the agent has been initialized, you can access the module's functionality through `agent.modules.openId4VcIssuer`. - -#### Preparing a DID diff --git a/packages/openid4vc/src/openid4vc-issuer/__tests__/openId4vc-issuer-module.test.ts b/packages/openid4vc/src/openid4vc-issuer/__tests__/openId4vc-issuer-module.test.ts index 749405ca1f..9e23a8daab 100644 --- a/packages/openid4vc/src/openid4vc-issuer/__tests__/openId4vc-issuer-module.test.ts +++ b/packages/openid4vc/src/openid4vc-issuer/__tests__/openId4vc-issuer-module.test.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/unbound-method */ +import type { IssuerMetadata } from '../OpenId4VcIssuerServiceOptions' import type { DependencyManager } from '@aries-framework/core' import { OpenId4VcIssuerApi } from '../OpenId4VcIssuerApi' @@ -15,10 +16,10 @@ const dependencyManager = { describe('OpenId4VcIssuerModule', () => { test('registers dependencies on the dependency manager', () => { - const issuerMetadata = { - credentialIssuer: 'https://example.com', - credentialEndpoint: 'https://example.com/credentials', - tokenEndpoint: 'https://example.com/token', + const issuerMetadata: IssuerMetadata = { + issuerBaseUrl: 'https://example.com', + credentialEndpointPath: 'https://example.com/credentials', + tokenEndpointPath: 'https://example.com/token', credentialsSupported: [], } const openId4VcClientModule = new OpenId4VcIssuerModule({ diff --git a/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.e2e.test.ts b/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.e2e.test.ts index 272b0ab102..0e6353ae9a 100644 --- a/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.e2e.test.ts @@ -36,11 +36,11 @@ import { w3cDate, } from '@aries-framework/core' import { agentDependencies } from '@aries-framework/node' -import { SdJwtVcApi, SdJwtVcModule } from '@aries-framework/sd-jwt-vc' +import { SdJwtCredential, SdJwtVcApi, SdJwtVcModule } from '@aries-framework/sd-jwt-vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { cleanAll, enableNetConnect } from 'nock' -import { OpenId4VcIssuerModule, OpenId4VcIssuerService } from '..' +import { OpenId4VcIssuerModule, OpenId4VcIssuerModuleConfig } from '..' import { OpenIdCredentialFormatProfile } from '../../openid4vc-holder' type CredentialSupportedWithId = CredentialSupported & { id: string } @@ -78,9 +78,9 @@ const baseCredentialRequestOptions = { } const issuerMetadata: IssuerMetadata = { - credentialIssuer: 'https://openid4vc-issuer.com', - credentialEndpoint: 'https://openid4vc-issuer.com/credentials', - tokenEndpoint: 'https://openid4vc-issuer.com/token', + issuerBaseUrl: 'https://openid4vc-issuer.com', + tokenEndpointPath: '/token', + credentialEndpointPath: '/credentials', credentialsSupported: [openBadgeCredential, universityDegreeCredentialLd, universityDegreeCredentialSdJwt], } @@ -104,7 +104,7 @@ const createCredentialRequest = async ( ): Promise => { const { credentialSupported, kid, nonce, issuerMetadata, clientId } = options - const aud = issuerMetadata.credentialIssuer + const aud = issuerMetadata.issuerBaseUrl const didsApi = agentContext.dependencyManager.resolve(DidsApi) const didDocument = await didsApi.resolveDidDocument(kid) @@ -161,15 +161,13 @@ describe('OpenId4VcIssuer', () => { let holderVerificationMethod: VerificationMethod let holderDid: string - let issuerService: OpenId4VcIssuerService - beforeEach(async () => { issuer = new Agent({ config: { - label: 'OpenId4VcIssuer Test321', + label: 'OpenId4VcIssuer Test323', walletConfig: { - id: 'openid4vc-Issuer-test321', - key: 'openid4vc-Issuer-test321', + id: 'openid4vc-Issuer-test323', + key: 'openid4vc-Issuer-test323', }, }, dependencies: agentDependencies, @@ -178,10 +176,10 @@ describe('OpenId4VcIssuer', () => { holder = new Agent({ config: { - label: 'OpenId4VciIssuer(Holder) Test321', + label: 'OpenId4VciIssuer(Holder) Test323', walletConfig: { - id: 'openid4vc-Issuer(Holder)-test321', - key: 'openid4vc-Issuer(Holder)-test321', + id: 'openid4vc-Issuer(Holder)-test323', + key: 'openid4vc-Issuer(Holder)-test323', }, }, dependencies: agentDependencies, @@ -221,8 +219,6 @@ describe('OpenId4VcIssuer', () => { ]) if (!_issuerVerificationMethod) throw new Error('No verification method found') issuerVerificationMethod = _issuerVerificationMethod - - issuerService = issuer.context.dependencyManager.resolve(OpenId4VcIssuerService) }) afterEach(async () => { @@ -283,7 +279,10 @@ describe('OpenId4VcIssuer', () => { const cNonce = '1234' const preAuthorizedCode = '1234567890' - await issuerService.cNonceStateManager.set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) + await issuer.context.dependencyManager + .resolve(OpenId4VcIssuerModuleConfig) + .getCNonceStateManager(issuer.context) + .set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, userPinRequired: false } @@ -299,18 +298,15 @@ describe('OpenId4VcIssuer', () => { 'openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FUniversityDegreeCredentialSdJwt%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%22%7D' ) - // TODO: - const { compact } = await issuer.modules.sdJwtVc.create( - { type: 'UniversityDegreeCredential', university: 'innsbruck', degree: 'bachelor' }, - { - holderDidUrl: holderVerificationMethod.id, - issuerDidUrl: issuerVerificationMethod.id, - disclosureFrame: { university: true, degree: true }, - } - ) + const sdJwtCredential = new SdJwtCredential({ + payload: { type: 'UniversityDegreeCredential', university: 'innsbruck', degree: 'bachelor' }, + holderDidUrl: holderVerificationMethod.id, + issuerDidUrl: issuerVerificationMethod.id, + disclosureFrame: { university: true, degree: true }, + }) const issueCredentialResponse = await issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ - credential: compact, + credential: sdJwtCredential, verificationMethod: issuerVerificationMethod, credentialRequest: await createCredentialRequest(holder.context, { credentialSupported: universityDegreeCredentialSdJwt, @@ -330,7 +326,10 @@ describe('OpenId4VcIssuer', () => { const cNonce = '1234' const preAuthorizedCode = '1234567890' - await issuerService.cNonceStateManager.set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) + await issuer.context.dependencyManager + .resolve(OpenId4VcIssuerModuleConfig) + .getCNonceStateManager(issuer.context) + .set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, userPinRequired: false } @@ -371,7 +370,10 @@ describe('OpenId4VcIssuer', () => { const cNonce = '1234' const preAuthorizedCode = '1234567890' - await issuerService.cNonceStateManager.set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) + await issuer.context.dependencyManager + .resolve(OpenId4VcIssuerModuleConfig) + .getCNonceStateManager(issuer.context) + .set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, userPinRequired: false } @@ -391,7 +393,10 @@ describe('OpenId4VcIssuer', () => { const cNonce = '1234' const preAuthorizedCode = '1234567890' - await issuerService.cNonceStateManager.set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) + await issuer.context.dependencyManager + .resolve(OpenId4VcIssuerModuleConfig) + .getCNonceStateManager(issuer.context) + .set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, userPinRequired: false } @@ -429,7 +434,10 @@ describe('OpenId4VcIssuer', () => { const cNonce = '1234' const preAuthorizedCode = '1234567890' - await issuerService.cNonceStateManager.set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) + await issuer.context.dependencyManager + .resolve(OpenId4VcIssuerModuleConfig) + .getCNonceStateManager(issuer.context) + .set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, userPinRequired: false } @@ -473,7 +481,10 @@ describe('OpenId4VcIssuer', () => { const cNonce = '1234' const preAuthorizedCode = '1234567890' - await issuerService.cNonceStateManager.set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) + await issuer.context.dependencyManager + .resolve(OpenId4VcIssuerModuleConfig) + .getCNonceStateManager(issuer.context) + .set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, @@ -518,7 +529,10 @@ describe('OpenId4VcIssuer', () => { const cNonce = '1234' const issuerState = '1234567890' - await issuerService.cNonceStateManager.set(cNonce, { cNonce: cNonce, createdAt: Date.now(), issuerState }) + await issuer.context.dependencyManager + .resolve(OpenId4VcIssuerModuleConfig) + .getCNonceStateManager(issuer.context) + .set(cNonce, { cNonce: cNonce, createdAt: Date.now(), issuerState }) const authorizationCodeFlowConfig: AuthorizationCodeFlowConfig = { issuerState } @@ -607,7 +621,10 @@ describe('OpenId4VcIssuer', () => { const cNonce = '1234' const preAuthorizedCode = '1234567890' - await issuerService.cNonceStateManager.set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) + await issuer.context.dependencyManager + .resolve(OpenId4VcIssuerModuleConfig) + .getCNonceStateManager(issuer.context) + .set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, userPinRequired: false } diff --git a/packages/openid4vc/src/openid4vc-issuer/__tests__/setup.ts b/packages/openid4vc/src/openid4vc-issuer/__tests__/setup.ts deleted file mode 100644 index 34e38c9705..0000000000 --- a/packages/openid4vc/src/openid4vc-issuer/__tests__/setup.ts +++ /dev/null @@ -1 +0,0 @@ -jest.setTimeout(120000) diff --git a/packages/openid4vc/src/openid4vc-issuer/router/OpenId4VcIEndpointConfiguration.ts b/packages/openid4vc/src/openid4vc-issuer/router/OpenId4VcIEndpointConfiguration.ts index 2386a46fa6..5100104736 100644 --- a/packages/openid4vc/src/openid4vc-issuer/router/OpenId4VcIEndpointConfiguration.ts +++ b/packages/openid4vc/src/openid4vc-issuer/router/OpenId4VcIEndpointConfiguration.ts @@ -1,115 +1,57 @@ +import type { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' import type { - IssuerMetadata, - CreateIssueCredentialResponseOptions, - MetadataEndpointConfig, - CredentialEndpointConfig, AccessTokenEndpointConfig, - CredentialSupported, + CredentialEndpointConfig, + MetadataEndpointConfig, } from '../OpenId4VcIssuerServiceOptions' -import type { - CNonceState, - CredentialIssuerMetadata, - CredentialOfferSession, - CredentialRequestV1_0_11, - CredentialResponse, - IStateManager, -} from '@sphereon/oid4vci-common' -import type { Router, Request, Response } from 'express' +import type { AgentContext, Logger } from '@aries-framework/core' +import type { CredentialRequestV1_0_11 } from '@sphereon/oid4vci-common' +import type { Request, Response, Router } from 'express' -import { Jwt, type AgentContext, type Logger, DidsApi, AriesFrameworkError } from '@aries-framework/core' +import { AriesFrameworkError, DidsApi, Jwt } from '@aries-framework/core' +import { getRequestContext, sendErrorResponse } from './../../shared/router' import { handleTokenRequest, verifyTokenRequest } from './accessTokenEndpoint' -import { getEndpointMetadata, sendErrorResponse } from './utils' -export interface InternalMetadataEndpointConfig extends MetadataEndpointConfig { - issuerMetadata: IssuerMetadata +export interface IssuanceRequestContext { + agentContext: AgentContext + openId4vcIssuerService: OpenId4VcIssuerService + logger: Logger } -export function configureIssuerMetadataEndpoint( - router: Router, - logger: Logger, - config: InternalMetadataEndpointConfig -) { - const { issuerMetadata } = config - const wellKnownPath = `/.well-known/openid-credential-issuer` - - const { path, url } = getEndpointMetadata(wellKnownPath, issuerMetadata.credentialIssuer) - logger.info(`[OID4VCI] metadata hosted at '${url.toString()}'.`) - - const transformedMetadata: CredentialIssuerMetadata = { - credentials_supported: issuerMetadata.credentialsSupported as CredentialSupported[], - credential_endpoint: issuerMetadata.credentialEndpoint, - authorization_server: issuerMetadata.authorizationServer, - credential_issuer: issuerMetadata.credentialIssuer, - display: issuerMetadata.issuerDisplay ? [issuerMetadata.issuerDisplay] : undefined, - token_endpoint: issuerMetadata.tokenEndpoint, - } - - router.get(path, (_request: Request, response: Response) => { - response.status(200).json(transformedMetadata) - }) +export interface IssuanceRequest extends Request { + requestContext?: IssuanceRequestContext } +export type InternalMetadataEndpointConfig = MetadataEndpointConfig + export interface InternalAccessTokenEndpointConfig extends AccessTokenEndpointConfig { - issuerMetadata: IssuerMetadata cNonceExpiresIn: number tokenExpiresIn: number - cNonceStateManager: IStateManager - credentialOfferSessionManager: IStateManager } export function configureAccessTokenEndpoint( - agentContext: AgentContext, router: Router, - logger: Logger, + pathname: string, config: InternalAccessTokenEndpointConfig ) { - const { issuerMetadata, credentialOfferSessionManager, preAuthorizedCodeExpirationDuration } = config - - const { path, url } = getEndpointMetadata(issuerMetadata.tokenEndpoint, issuerMetadata.credentialIssuer) - logger.info(`[OID4VCI] Token endpoint running at '${url.toString()}'.`) - - router.post( - path, - - verifyTokenRequest({ - logger, - credentialOfferSessionManager, - preAuthorizedCodeExpirationDuration, - }), - - handleTokenRequest(agentContext, logger, config) - ) + const { preAuthorizedCodeExpirationDuration } = config + router.post(pathname, verifyTokenRequest({ preAuthorizedCodeExpirationDuration }), handleTokenRequest(config)) } -export interface InternalCredentialEndpointConfig extends CredentialEndpointConfig { - issuerMetadata: IssuerMetadata - cNonceStateManager: IStateManager - credentialOfferSessionManager: IStateManager - createIssueCredentialResponse: ( - agentContext: AgentContext, - options: CreateIssueCredentialResponseOptions - ) => Promise -} +export type InternalCredentialEndpointConfig = CredentialEndpointConfig export function configureCredentialEndpoint( - agentContext: AgentContext, router: Router, - logger: Logger, + pathname: string, config: InternalCredentialEndpointConfig -): void { - const { - issuerMetadata, - credentialRequestToCredentialMapper, - verificationMethod, - cNonceStateManager, - credentialOfferSessionManager, - } = config - - const { path, url } = getEndpointMetadata(issuerMetadata.credentialEndpoint, issuerMetadata.credentialIssuer) - logger.info(`[OID4VCI] Token endpoint running at '${url.toString()}'.`) - - router.post(path, async (request: Request, response: Response) => { +) { + const { credentialRequestToCredentialMapper, verificationMethod } = config + + router.post(pathname, async (request: IssuanceRequest, response: Response) => { + const requestContext = getRequestContext(request) + const { agentContext, openId4vcIssuerService, logger } = requestContext + try { const credentialRequest = request.body as CredentialRequestV1_0_11 @@ -117,9 +59,7 @@ export function configureCredentialEndpoint( const jwt = Jwt.fromSerializedJwt(credentialRequest.proof?.jwt) const kid = jwt.header.kid - if (!kid) { - throw new AriesFrameworkError('Received a credential request without a kid') - } + if (!kid) throw new AriesFrameworkError('Received a credential request without a kid') const didsApi = agentContext.dependencyManager.resolve(DidsApi) const didDocument = await didsApi.resolveDidDocument(kid) @@ -130,7 +70,9 @@ export function configureCredentialEndpoint( throw new AriesFrameworkError(`Received a credential request without a valid nonce. ${requestNonce}`) } - const cNonceState = await cNonceStateManager.get(requestNonce) + const cNonceState = await openId4vcIssuerService.openId4VcIssuerModuleConfig + .getCNonceStateManager(agentContext) + .get(requestNonce) const credentialOfferSessionId = cNonceState?.preAuthorizedCode ?? cNonceState?.issuerState @@ -140,25 +82,30 @@ export function configureCredentialEndpoint( ) } - const credentialOfferSession = await credentialOfferSessionManager.get(credentialOfferSessionId) + const credentialOfferSession = await openId4vcIssuerService.openId4VcIssuerModuleConfig + .getCredentialOfferSessionStateManager(agentContext) + .get(credentialOfferSessionId) + if (!credentialOfferSession) throw new AriesFrameworkError( `Credential offer session for request nonce '${requestNonce}' with id '${credentialOfferSessionId}' not found.` ) - const credential = await credentialRequestToCredentialMapper(credentialRequest, { + const credential = await credentialRequestToCredentialMapper({ + agentContext, + credentialRequest, holderDid, holderDidUrl: kid, cNonceState, credentialOfferSession, }) - const issueCredentialResponse = await config.createIssueCredentialResponse(agentContext, { + const issueCredentialResponse = await openId4vcIssuerService.createIssueCredentialResponse(agentContext, { credentialRequest, - issuerMetadata, verificationMethod, credential, }) + return response.send(issueCredentialResponse) } catch (e) { sendErrorResponse(response, logger, 500, 'invalid_request', e) diff --git a/packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts b/packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts index 5c6e3bcd45..5b7ef28c51 100644 --- a/packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts @@ -1,15 +1,15 @@ -import type { InternalAccessTokenEndpointConfig } from './OpenId4VcIEndpointConfiguration' -import type { AgentContext, Logger, VerificationMethod, JwkJson } from '@aries-framework/core' -import type { CredentialOfferSession, IStateManager, JWTSignerCallback } from '@sphereon/oid4vci-common' -import type { NextFunction, Request, Response } from 'express' +import type { InternalAccessTokenEndpointConfig, IssuanceRequest } from './OpenId4VcIEndpointConfiguration' +import type { AgentContext, JwkJson, VerificationMethod } from '@aries-framework/core' +import type { JWTSignerCallback } from '@sphereon/oid4vci-common' +import type { NextFunction, Response } from 'express' import { AriesFrameworkError, JwsService, - getJwkFromJson, - getKeyFromVerificationMethod, JwtPayload, getJwkClassFromKeyType, + getJwkFromJson, + getKeyFromVerificationMethod, } from '@aries-framework/core' import { GrantTypes, @@ -19,7 +19,7 @@ import { } from '@sphereon/oid4vci-common' import { assertValidAccessTokenRequest, createAccessTokenResponse } from '@sphereon/oid4vci-issuer' -import { sendErrorResponse } from './utils' +import { getRequestContext, sendErrorResponse } from '../../shared/router' const getJwtSignerCallback = ( agentContext: AgentContext, @@ -46,16 +46,15 @@ const getJwtSignerCallback = ( } } -export const handleTokenRequest = ( - agentContext: AgentContext, - logger: Logger, - config: InternalAccessTokenEndpointConfig -) => { +export const handleTokenRequest = (config: InternalAccessTokenEndpointConfig) => { const { tokenExpiresIn, cNonceExpiresIn, interval } = config - return async (request: Request, response: Response) => { + return async (request: IssuanceRequest, response: Response) => { response.set({ 'Cache-Control': 'no-store', Pragma: 'no-cache' }) + const requestContext = getRequestContext(request) + const { agentContext, openId4vcIssuerService, logger } = requestContext + if (request.body.grant_type !== GrantTypes.PRE_AUTHORIZED_CODE) { return response.status(400).json({ error: TokenErrorResponse.invalid_request, @@ -65,12 +64,13 @@ export const handleTokenRequest = ( try { const accessTokenResponse = await createAccessTokenResponse(request.body, { - credentialOfferSessions: config.credentialOfferSessionManager, + credentialOfferSessions: + openId4vcIssuerService.openId4VcIssuerModuleConfig.getCredentialOfferSessionStateManager(agentContext), tokenExpiresIn, - accessTokenIssuer: config.issuerMetadata.credentialIssuer, + accessTokenIssuer: openId4vcIssuerService.expandEndpointsWithBase(agentContext).issuerBaseUrl, cNonce: await agentContext.wallet.generateNonce(), cNonceExpiresIn, - cNonces: config.cNonceStateManager, + cNonces: openId4vcIssuerService.openId4VcIssuerModuleConfig.getCNonceStateManager(agentContext), accessTokenSignerCallback: getJwtSignerCallback(agentContext, config.verificationMethod), interval, }) @@ -81,18 +81,18 @@ export const handleTokenRequest = ( } } -export const verifyTokenRequest = (options: { - preAuthorizedCodeExpirationDuration: number - credentialOfferSessionManager: IStateManager - logger: Logger -}) => { - const { preAuthorizedCodeExpirationDuration, credentialOfferSessionManager, logger } = options - return async (request: Request, response: Response, next: NextFunction) => { +export const verifyTokenRequest = (options: { preAuthorizedCodeExpirationDuration: number }) => { + const { preAuthorizedCodeExpirationDuration } = options + return async (request: IssuanceRequest, response: Response, next: NextFunction) => { + const requestContext = getRequestContext(request) + const { agentContext, openId4vcIssuerService, logger } = requestContext + try { await assertValidAccessTokenRequest(request.body, { // we use seconds instead of milliseconds for consistency expirationDuration: preAuthorizedCodeExpirationDuration * 1000, - credentialOfferSessions: credentialOfferSessionManager, + credentialOfferSessions: + openId4vcIssuerService.openId4VcIssuerModuleConfig.getCredentialOfferSessionStateManager(agentContext), }) } catch (error) { if (error instanceof TokenError) { diff --git a/packages/openid4vc/src/openid4vc-issuer/router/metadataEndpoint.ts b/packages/openid4vc/src/openid4vc-issuer/router/metadataEndpoint.ts new file mode 100644 index 0000000000..ec9659928a --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/router/metadataEndpoint.ts @@ -0,0 +1,25 @@ +import type { IssuanceRequest } from './OpenId4VcIEndpointConfiguration' +import type { CredentialIssuerMetadata, CredentialSupported } from '@sphereon/oid4vci-common' +import type { Router, Response } from 'express' + +import { getRequestContext, sendErrorResponse } from '../../shared/router' + +export function configureIssuerMetadataEndpoint(router: Router, pathname: string) { + router.get(pathname, (_request: IssuanceRequest, response: Response) => { + const { agentContext, openId4vcIssuerService, logger } = getRequestContext(_request) + try { + const metadata = openId4vcIssuerService.expandEndpointsWithBase(agentContext) + const transformedMetadata: CredentialIssuerMetadata = { + credential_issuer: metadata.issuerBaseUrl, + token_endpoint: metadata.tokenEndpointPath, + credential_endpoint: metadata.credentialEndpointPath, + authorization_server: metadata.authorizationServerUrl, + credentials_supported: metadata.credentialsSupported as CredentialSupported[], + display: metadata.issuerDisplay ? [metadata.issuerDisplay] : undefined, + } + response.status(200).json(transformedMetadata) + } catch (e) { + sendErrorResponse(response, logger, 500, 'invalid_request', e) + } + }) +} diff --git a/packages/openid4vc/src/openid4vc-issuer/router/utils.ts b/packages/openid4vc/src/openid4vc-issuer/router/utils.ts deleted file mode 100644 index c3501bd3d6..0000000000 --- a/packages/openid4vc/src/openid4vc-issuer/router/utils.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { Logger } from '@aries-framework/core' -import type { Response } from 'express' - -import { AriesFrameworkError } from '@aries-framework/core' - -export function sendErrorResponse(response: Response, logger: Logger, code: number, message: string, error: unknown) { - const error_description = - error instanceof Error ? error.message : typeof error === 'string' ? error : 'An unknown error occurred.' - - const body = { error: message, error_description } - logger.warn(`[OID4VCI] Sending error response: ${JSON.stringify(body)}`) - - return response.status(code).json(body) -} - -export function getEndpointMetadata(endpoint: string, base: string) { - const baseUrl = new URL(base) - - // if the endpoint is relative, append it to origin of the base - // if the endpoint is absolute, use the pathname - const url = new URL(endpoint, baseUrl) - - if (url.protocol !== 'http:' && url.protocol !== 'https:') { - throw new AriesFrameworkError(`Endpoint '${endpoint}' is not valid. Invalid protocol '${url.protocol}'`) - } - - if (url.origin !== baseUrl.origin) { - throw new AriesFrameworkError(`Endpoint '${endpoint}' is not valid. Invalid origin '${url}'`) - } - - const path = url.pathname.replace(/\/$/, '') - - return { path, url } -} diff --git a/packages/openid4vc/src/openid4vc-verifier/InMemoryVerifierSessionManager.ts b/packages/openid4vc/src/openid4vc-verifier/InMemoryVerifierSessionManager.ts index b9334f0841..c880c2d755 100644 --- a/packages/openid4vc/src/openid4vc-verifier/InMemoryVerifierSessionManager.ts +++ b/packages/openid4vc/src/openid4vc-verifier/InMemoryVerifierSessionManager.ts @@ -28,7 +28,7 @@ export type PresentationDefinitionForCorrelationId = } export interface IInMemoryVerifierSessionManager extends SphereonRPSessionManager { - getVerifiyProofResponseOptions(correlationId: string): Promise + getVerifyProofResponseOptions(correlationId: string): Promise saveVerifyProofResponseOptions( correlationId: string, presentationDefinitionForCorrelationId: VerifyProofResponseOptions @@ -60,37 +60,19 @@ export class InMemoryVerifierSessionManager implements IInMemoryVerifierSessionM .map((filtered) => Number.parseInt(filtered[0])) } - public constructor(eventEmitter: EventEmitter, logger: Logger, opts?: { maxAgeInSeconds?: number }) { + public constructor(ee: EventEmitter, logger: Logger, opts?: { maxAgeInSeconds?: number }) { this.logger = logger this.maxAgeInSeconds = opts?.maxAgeInSeconds ?? 5 * 60 - eventEmitter.on( - AuthorizationEvents.ON_AUTH_REQUEST_CREATED_SUCCESS, - this.onAuthorizationRequestCreatedSuccess.bind(this) - ) - eventEmitter.on( - AuthorizationEvents.ON_AUTH_REQUEST_CREATED_FAILED, - this.onAuthorizationRequestCreatedFailed.bind(this) - ) - eventEmitter.on(AuthorizationEvents.ON_AUTH_REQUEST_SENT_SUCCESS, this.onAuthorizationRequestSentSuccess.bind(this)) - eventEmitter.on(AuthorizationEvents.ON_AUTH_REQUEST_SENT_FAILED, this.onAuthorizationRequestSentFailed.bind(this)) - eventEmitter.on( - AuthorizationEvents.ON_AUTH_RESPONSE_RECEIVED_SUCCESS, - this.onAuthorizationResponseReceivedSuccess.bind(this) - ) - eventEmitter.on( - AuthorizationEvents.ON_AUTH_RESPONSE_RECEIVED_FAILED, - this.onAuthorizationResponseReceivedFailed.bind(this) - ) - eventEmitter.on( - AuthorizationEvents.ON_AUTH_RESPONSE_VERIFIED_SUCCESS, - this.onAuthorizationResponseVerifiedSuccess.bind(this) - ) - eventEmitter.on( - AuthorizationEvents.ON_AUTH_RESPONSE_VERIFIED_FAILED, - this.onAuthorizationResponseVerifiedFailed.bind(this) - ) + ee.on(AuthorizationEvents.ON_AUTH_REQUEST_CREATED_SUCCESS, this.onAuthorizationRequestCreatedSuccess.bind(this)) + ee.on(AuthorizationEvents.ON_AUTH_REQUEST_CREATED_FAILED, this.onAuthorizationRequestCreatedFailed.bind(this)) + ee.on(AuthorizationEvents.ON_AUTH_REQUEST_SENT_SUCCESS, this.onAuthorizationRequestSentSuccess.bind(this)) + ee.on(AuthorizationEvents.ON_AUTH_REQUEST_SENT_FAILED, this.onAuthorizationRequestSentFailed.bind(this)) + ee.on(AuthorizationEvents.ON_AUTH_RESPONSE_RECEIVED_SUCCESS, this.onAuthorizationResponseReceivedSuccess.bind(this)) + ee.on(AuthorizationEvents.ON_AUTH_RESPONSE_RECEIVED_FAILED, this.onAuthorizationResponseReceivedFailed.bind(this)) + ee.on(AuthorizationEvents.ON_AUTH_RESPONSE_VERIFIED_SUCCESS, this.onAuthorizationResponseVerifiedSuccess.bind(this)) + ee.on(AuthorizationEvents.ON_AUTH_RESPONSE_VERIFIED_FAILED, this.onAuthorizationResponseVerifiedFailed.bind(this)) } - public async getVerifiyProofResponseOptions(correlationId: string): Promise { + public async getVerifyProofResponseOptions(correlationId: string): Promise { return this.correlationIdToVerifyProofResponseOptions[correlationId] } diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierApi.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierApi.ts index fbc7982379..8eb3d1051e 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierApi.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierApi.ts @@ -1,4 +1,10 @@ -import type { CreateProofRequestOptions, VerifierEndpointConfig, ProofPayload } from './OpenId4VcVerifierServiceOptions' +import type { + CreateProofRequestOptions, + VerifierEndpointConfig, + ProofPayload, + ProofRequestWithMetadata, + VerifiedProofResponse, +} from './OpenId4VcVerifierServiceOptions' import type { Router } from 'express' import { injectable, AgentContext } from '@aries-framework/core' @@ -26,14 +32,14 @@ export class OpenId4VcVerifierApi { * If neither the holder metadata nor the issuer URL is provided, a static configuration defined in @link https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-static-configuration-values * If a presentation definition is provided, a VP request will be created, querying the holder verifiable credentials according to the specifics of the presentation definition. * - * @param options.redirectUri - The URL to redirect to after verification. + * @param options.verificationMethod - The VerificationMethod used for signing the proof request. * @param options.holderMetadata - Optional metadata about the holder. * @param options.holderIdentifier - Optional the identifier of the holder (OpenId-Provider) provider for performing dynamic discovery. How to identifier is obtained is out of scope. * @param options.presentationDefinition - Optional presentation definition for requesting the presentation of verifiable credentials. - * @param options.verificationMethod - The VerificationMethod to use for signing the proof request. + * @param options.verificationEndpointUrl - Optional The URL to where the holder will send the response. * @returns @see ProofRequestWithMetadata object containing the proof request and metadata for verifying the proof response. */ - public async createProofRequest(options: CreateProofRequestOptions) { + public async createProofRequest(options: CreateProofRequestOptions): Promise { return await this.openId4VcVerifierService.createProofRequest(this.agentContext, options) } @@ -47,7 +53,7 @@ export class OpenId4VcVerifierApi { * @param options.proofRequestMetadata - Metadata about the proof request. * @returns @see VerifiedProofResponse object containing the idTokenPayload and the verified submission. */ - public async verifyProofResponse(proofPayload: ProofPayload) { + public async verifyProofResponse(proofPayload: ProofPayload): Promise { return await this.openId4VcVerifierService.verifyProofResponse(this.agentContext, proofPayload) } diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts index c275f67ccb..473f2543c8 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts @@ -24,11 +24,10 @@ export class OpenId4VcVerifierModule implements Module { */ public register(dependencyManager: DependencyManager) { // Warn about experimental module - dependencyManager - .resolve(AgentConfig) - .logger.warn( - "The '@aries-framework/openid4vc-verifier' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages. Multi-Tenancy is not supported. " - ) + const logger = dependencyManager.resolve(AgentConfig).logger + logger.warn( + "The '@aries-framework/openid4vc-verifier' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages. Multi-Tenancy is not supported. " + ) // Register config dependencyManager.registerInstance(OpenId4VcVerifierModuleConfig, this.config) diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts index d274c35bc5..3abe7fc372 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts @@ -1,17 +1,59 @@ -import type { IInMemoryVerifierSessionManager } from './InMemoryVerifierSessionManager' +import type { VerifierMetadata } from './OpenId4VcVerifierServiceOptions' + +import { AgentConfig, type AgentContext } from '@aries-framework/core' +import { EventEmitter } from 'events' + +import { InMemoryVerifierSessionManager, type IInMemoryVerifierSessionManager } from './InMemoryVerifierSessionManager' export interface OpenId4VcVerifierModuleConfigOptions { - sessionManager?: IInMemoryVerifierSessionManager + verifierMetadata: VerifierMetadata + sessionManagerFactory?: () => IInMemoryVerifierSessionManager } export class OpenId4VcVerifierModuleConfig { private options: OpenId4VcVerifierModuleConfigOptions + private basePathMap: Map + private eventEmitterMap: Map + private sessionManagerMap: Map public constructor(options: OpenId4VcVerifierModuleConfigOptions) { this.options = options + this.sessionManagerMap = new Map() + this.eventEmitterMap = new Map() + this.basePathMap = new Map() + } + + public getSessionManager(agentContext: AgentContext) { + const val = this.sessionManagerMap.get(agentContext.contextCorrelationId) + if (val) return val + + const logger = agentContext.dependencyManager.resolve(AgentConfig).logger + + const newVal = + this.options.sessionManagerFactory?.() ?? + new InMemoryVerifierSessionManager(this.getEventEmitter(agentContext), logger) + this.sessionManagerMap.set(agentContext.contextCorrelationId, newVal) + return newVal + } + + public getEventEmitter(agentConext: AgentContext) { + const val = this.eventEmitterMap.get(agentConext.contextCorrelationId) + if (val) return val + + const newVal = new EventEmitter() + this.eventEmitterMap.set(agentConext.contextCorrelationId, newVal) + return newVal + } + + public getBasePath(agentContext: AgentContext): string { + return this.basePathMap.get(agentContext.contextCorrelationId) ?? '/' + } + + public setBasePath(agentContext: AgentContext, basePath: string): void { + this.basePathMap.set(agentContext.contextCorrelationId, basePath) } - public get sessionManager() { - return this.options.sessionManager + public get verifierMetadata() { + return this.options.verifierMetadata } } diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierService.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierService.ts index 0ae8bb27af..345e06702b 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierService.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierService.ts @@ -1,19 +1,19 @@ -import type { IInMemoryVerifierSessionManager } from './InMemoryVerifierSessionManager' import type { ProofRequestWithMetadata, CreateProofRequestOptions, ProofRequestMetadata, VerifiedProofResponse, VerifierEndpointConfig, + HolderMetadata, } from './OpenId4VcVerifierServiceOptions' +import type { VerificationRequest } from './router/OpenId4VpEndpointConfiguration' import type { AgentContext, W3cVerifyPresentationResult } from '@aries-framework/core' import type { AuthorizationResponsePayload, - ClientMetadataOpts, PresentationVerificationCallback, SigningAlgo, } from '@sphereon/did-auth-siop' -import type { Router } from 'express' +import type { NextFunction, Response, Router } from 'express' import { InjectionSymbols, @@ -24,6 +24,7 @@ import { AriesFrameworkError, W3cJsonLdVerifiablePresentation, JsonTransformer, + AgentContextProvider, } from '@aries-framework/core' import { RP, @@ -40,17 +41,19 @@ import { AuthorizationResponse, } from '@sphereon/did-auth-siop' import bodyParser from 'body-parser' -import { EventEmitter } from 'events' -import { InMemoryVerifierSessionManager } from './InMemoryVerifierSessionManager' -import { OpenId4VcVerifierModuleConfig } from './OpenId4VcVerifierModuleConfig' -import { staticOpOpenIdConfig, staticOpSiopConfig } from './OpenId4VcVerifierServiceOptions' +import { getRequestContext, getEndpointUrl, initializeAgentFromContext } from '../shared/router' import { + generateRandomValues, getSupportedDidMethods, getSuppliedSignatureFromVerificationMethod, getResolver, getSupportedJwaSignatureAlgorithms, -} from './utils' +} from '../shared/utils' + +import { OpenId4VcVerifierModuleConfig } from './OpenId4VcVerifierModuleConfig' +import { staticOpOpenIdConfig, staticOpSiopConfig } from './OpenId4VcVerifierServiceOptions' +import { configureVerificationEndpoint } from './router/OpenId4VpEndpointConfiguration' /** * @internal @@ -60,20 +63,22 @@ export class OpenId4VcVerifierService { private logger: Logger private w3cCredentialService: W3cCredentialService private openId4VcVerifierModuleConfig: OpenId4VcVerifierModuleConfig - private sessionManager: IInMemoryVerifierSessionManager - private eventEmitter: EventEmitter + private agentContextProvider: AgentContextProvider + + public get verifierMetadata() { + return this.openId4VcVerifierModuleConfig.verifierMetadata + } public constructor( @inject(InjectionSymbols.Logger) logger: Logger, + @inject(InjectionSymbols.AgentContextProvider) agentContextProvider: AgentContextProvider, w3cCredentialService: W3cCredentialService, openId4VcVerifierModuleConfig: OpenId4VcVerifierModuleConfig ) { + this.agentContextProvider = agentContextProvider this.w3cCredentialService = w3cCredentialService this.logger = logger this.openId4VcVerifierModuleConfig = openId4VcVerifierModuleConfig - this.eventEmitter = new EventEmitter() - this.sessionManager = - openId4VcVerifierModuleConfig.sessionManager ?? new InMemoryVerifierSessionManager(this.eventEmitter, logger) } public async getRelyingParty( @@ -82,8 +87,7 @@ export class OpenId4VcVerifierService { proofRequestMetadata?: ProofRequestMetadata ) { const { - holderIdentifier, - redirectUri, + verificationEndpointUrl, presentationDefinition, verificationMethod, holderMetadata: _holderClientMetadata, @@ -91,23 +95,25 @@ export class OpenId4VcVerifierService { const isVpRequest = presentationDefinition !== undefined - let holderClientMetadata: ClientMetadataOpts - if (_holderClientMetadata) { - // use the provided client metadata - holderClientMetadata = _holderClientMetadata - } else if (holderIdentifier) { - // Use OpenId Discovery to get the client metadata - let reference_uri = holderIdentifier - if (!holderIdentifier.endsWith('/.well-known/openid-configuration')) { - reference_uri = holderIdentifier + '/.well-known/openid-configuration' + let holderClientMetadata: HolderMetadata + if (!_holderClientMetadata) { + // use a static set of configuration values defined in the spec + if (isVpRequest) { + holderClientMetadata = staticOpOpenIdConfig + } else { + holderClientMetadata = staticOpSiopConfig } - holderClientMetadata = { reference_uri, passBy: PassBy.REFERENCE, targets: PropertyTarget.REQUEST_OBJECT } - } else if (isVpRequest) { - // if neither clientMetadata nor issuer is provided, use a static config - holderClientMetadata = staticOpOpenIdConfig } else { - // if neither clientMetadata nor issuer is provided, use a static config - holderClientMetadata = staticOpSiopConfig + if (typeof _holderClientMetadata === 'string') { + // Use OpenId Discovery to get the client metadata + let reference_uri = _holderClientMetadata + if (!reference_uri.endsWith('/.well-known/openid-configuration')) { + reference_uri = reference_uri + '/.well-known/openid-configuration' + } + holderClientMetadata = { reference_uri, passBy: PassBy.REFERENCE, targets: PropertyTarget.REQUEST_OBJECT } + } else { + holderClientMetadata = _holderClientMetadata + } } const { signature, did, kid, alg } = await getSuppliedSignatureFromVerificationMethod( @@ -128,12 +134,14 @@ export class OpenId4VcVerifierService { // Check if the Relying Party (Verifier) can validate the IdToken provided by the OpenId Provider (Holder) const idTokenSigningAlgValuesSupported = holderClientMetadata.idTokenSigningAlgValuesSupported - const rpSupportedSignatureAlgorithms = getSupportedJwaSignatureAlgorithms(agentContext) as unknown as SigningAlgo[] - if (idTokenSigningAlgValuesSupported) { + const rpSupportedSignatureAlgorithms = getSupportedJwaSignatureAlgorithms( + agentContext + ) as unknown as SigningAlgo[] + const possibleIdTokenSigningAlgValues = Array.isArray(idTokenSigningAlgValuesSupported) ? idTokenSigningAlgValuesSupported.filter((value) => rpSupportedSignatureAlgorithms.includes(value)) - : [idTokenSigningAlgValuesSupported].filter((value) => rpSupportedSignatureAlgorithms.includes(value)) + : rpSupportedSignatureAlgorithms.includes(idTokenSigningAlgValuesSupported) if (!possibleIdTokenSigningAlgValues) { throw new AriesFrameworkError( @@ -146,9 +154,17 @@ export class OpenId4VcVerifierService { } } - const authorizationEndpoint = holderClientMetadata.authorization_endpoint ?? (isVpRequest ? 'openid:' : 'siopv2:') + const authorizationEndpoint = holderClientMetadata?.authorization_endpoint ?? (isVpRequest ? 'openid:' : 'siopv2:') - // Check: audience must be set to the issuer with dynamic disc otherwise self-issed.me/v2. + const redirectUri = + verificationEndpointUrl ?? + getEndpointUrl( + this.verifierMetadata.verifierBaseUrl, + this.openId4VcVerifierModuleConfig.getBasePath(agentContext), + this.verifierMetadata.verificationEndpointPath + ) + + // Check: audience must be set to the issuer with dynamic disc otherwise self-issued.me/v2. const builder = RP.builder() .withClientId(verificationMethod.id) .withRedirectUri(redirectUri) @@ -164,8 +180,8 @@ export class OpenId4VcVerifierService { .withAuthorizationEndpoint(authorizationEndpoint) .withCheckLinkedDomain(CheckLinkedDomain.NEVER) .withRevocationVerification(RevocationVerification.NEVER) - .withSessionManager(this.sessionManager) - .withEventEmitter(this.eventEmitter) + .withSessionManager(this.openId4VcVerifierModuleConfig.getSessionManager(agentContext)) + .withEventEmitter(this.openId4VcVerifierModuleConfig.getEventEmitter(agentContext)) // .withWellknownDIDVerifyCallback if (proofRequestMetadata) { @@ -209,10 +225,12 @@ export class OpenId4VcVerifierService { const proofRequestMetadata = { correlationId, challenge, state } - await this.sessionManager.saveVerifyProofResponseOptions(correlationId, { - createProofRequestOptions: options, - proofRequestMetadata, - }) + await this.openId4VcVerifierModuleConfig + .getSessionManager(agentContext) + .saveVerifyProofResponseOptions(correlationId, { + createProofRequestOptions: options, + proofRequestMetadata, + }) return { proofRequest: encodedAuthorizationRequestUri, @@ -233,33 +251,28 @@ export class OpenId4VcVerifierService { ) } - let correlationId: string | undefined const resNonce = (await authorizationResponse.getMergedProperty('nonce', false)) as string const resState = (await authorizationResponse.getMergedProperty('state', false)) as string - correlationId = await this.sessionManager.getCorrelationIdByNonce(resNonce, false) - if (!correlationId) { - correlationId = await this.sessionManager.getCorrelationIdByState(resState, false) - } + const sessionManager = this.openId4VcVerifierModuleConfig.getSessionManager(agentContext) + const correlationId = + (await sessionManager.getCorrelationIdByNonce(resNonce, false)) ?? + (await sessionManager.getCorrelationIdByState(resState, false)) if (!correlationId) { throw new AriesFrameworkError(`Unable to find correlationId for nonce '${resNonce}' or state '${resState}'`) } - const result = await this.sessionManager.getVerifiyProofResponseOptions(correlationId) - if (!result) { + const verifyProofResponseOptions = await sessionManager.getVerifyProofResponseOptions(correlationId) + if (!verifyProofResponseOptions) { throw new AriesFrameworkError(`Unable to associate a request to the response correlationId '${correlationId}'`) } - const { createProofRequestOptions, proofRequestMetadata } = result + const { createProofRequestOptions, proofRequestMetadata } = verifyProofResponseOptions const presentationDefinition = createProofRequestOptions.presentationDefinition + // For now we always use the VP_TOKEN const presentationDefinitionsWithLocation = presentationDefinition - ? [ - { - definition: presentationDefinition, - location: PresentationDefinitionLocation.CLAIMS_VP_TOKEN, // For now we always use the VP_TOKEN - }, - ] + ? [{ definition: presentationDefinition, location: PresentationDefinitionLocation.CLAIMS_VP_TOKEN }] : undefined const relyingParty = await this.getRelyingParty(agentContext, createProofRequestOptions, proofRequestMetadata) @@ -293,21 +306,17 @@ export class OpenId4VcVerifierService { this.logger.debug(`Presentation response`, JsonTransformer.toJSON(encodedPresentation)) this.logger.debug(`Presentation submission`, presentationSubmission) - if (!encodedPresentation) { - throw new AriesFrameworkError('Did not receive a presentation for verification') - } + if (!encodedPresentation) throw new AriesFrameworkError('Did not receive a presentation for verification.') let verificationResult: W3cVerifyPresentationResult if (typeof encodedPresentation === 'string') { - const presentation = encodedPresentation verificationResult = await this.w3cCredentialService.verifyPresentation(agentContext, { - presentation: presentation, + presentation: encodedPresentation, challenge, }) } else { - const presentation = JsonTransformer.fromJSON(encodedPresentation, W3cJsonLdVerifiablePresentation) verificationResult = await this.w3cCredentialService.verifyPresentation(agentContext, { - presentation: presentation, + presentation: JsonTransformer.fromJSON(encodedPresentation, W3cJsonLdVerifiablePresentation), challenge, }) } @@ -316,46 +325,52 @@ export class OpenId4VcVerifierService { } } - public configureRouter = (agentContext: AgentContext, router: Router, endpointConfig: VerifierEndpointConfig) => { + public configureRouter = ( + initializationContext: AgentContext, + router: Router, + endpointConfig: VerifierEndpointConfig + ) => { + const { basePath } = endpointConfig + this.openId4VcVerifierModuleConfig.setBasePath(initializationContext, basePath) + // parse application/x-www-form-urlencoded router.use(bodyParser.urlencoded({ extended: false })) // parse application/json router.use(bodyParser.json()) - if (endpointConfig.verificationEndpointConfig?.enabled) { - router.post( - endpointConfig.verificationEndpointConfig.verificationEndpointPath, - async (request, response, next) => { - try { - const isVpRequest = request.body.presentation_submission !== undefined - const verifierService = await agentContext.dependencyManager.resolve(OpenId4VcVerifierService) - - const authorizationResponse: AuthorizationResponsePayload = request.body - if (isVpRequest) - authorizationResponse.presentation_submission = JSON.parse(request.body.presentation_submission) - - const verifiedProofResponse = await verifierService.verifyProofResponse(agentContext, request.body) - if (!endpointConfig.verificationEndpointConfig.proofResponseHandler) return response.status(200).send() - - const { status } = await endpointConfig.verificationEndpointConfig.proofResponseHandler( - verifiedProofResponse - ) - return response.status(status).send() - } catch (error: unknown) { - next(error) - } - - return response.status(200).send() - } + // initialize the agent and set the request context + router.use(async (req: VerificationRequest, _res: Response, next: NextFunction) => { + const agentContext = await initializeAgentFromContext( + initializationContext.contextCorrelationId, + this.agentContextProvider ) + + req.requestContext = { + agentContext, + openId4VcVerifierService: agentContext.dependencyManager.resolve(OpenId4VcVerifierService), + logger: agentContext.dependencyManager.resolve(InjectionSymbols.Logger), + } + + next() + }) + + if (endpointConfig.verificationEndpointConfig?.enabled) { + const verificationEndpointPath = this.verifierMetadata.verificationEndpointPath + configureVerificationEndpoint(router, verificationEndpointPath, { + ...endpointConfig.verificationEndpointConfig, + }) + + const endPointUrl = getEndpointUrl(this.verifierMetadata.verifierBaseUrl, basePath, verificationEndpointPath) + this.logger.info(`[OID4VP] Verification endpoint running at '${endPointUrl}'.`) } + router.use(async (req: VerificationRequest, _res, next) => { + const { agentContext } = getRequestContext(req) + await agentContext.endSession() + next() + }) + return router } } - -async function generateRandomValues(agentContext: AgentContext, count: number) { - const randomValuesPromises = Array.from({ length: count }, () => agentContext.wallet.generateNonce()) - return await Promise.all(randomValuesPromises) -} diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierServiceOptions.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierServiceOptions.ts index 8772c5cd06..4686105980 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierServiceOptions.ts @@ -19,11 +19,21 @@ export type HolderMetadata = ClientMetadataOpts & { authorization_endpoint?: str export type { PresentationDefinitionV1, PresentationDefinitionV2, VerifiedOpenID4VPSubmission, IDTokenPayload } +export interface VerifierMetadata { + verifierBaseUrl: string + verificationEndpointPath: string +} + export interface CreateProofRequestOptions { verificationMethod: VerificationMethod - redirectUri: string - holderMetadata?: HolderMetadata - holderIdentifier?: string + verificationEndpointUrl?: string + + /** + * The holder metadata to use for the proof request. + * If not provided, a static set of configuration values defined in the spec will be used. + * If provided as a string (url), it will try to retrieve the metadata from the given url. + */ + holderMetadata?: HolderMetadata | string presentationDefinition?: PresentationDefinitionV1 | PresentationDefinitionV2 } @@ -82,10 +92,10 @@ export interface VerificationEndpointConfig { */ enabled: boolean - verificationEndpointPath: string proofResponseHandler?: ProofResponseHandler } export interface VerifierEndpointConfig { + basePath: string verificationEndpointConfig: VerificationEndpointConfig } diff --git a/packages/openid4vc/src/openid4vc-verifier/README.md b/packages/openid4vc/src/openid4vc-verifier/README.md deleted file mode 100644 index 02adb4c47a..0000000000 --- a/packages/openid4vc/src/openid4vc-verifier/README.md +++ /dev/null @@ -1,68 +0,0 @@ -

-
- Hyperledger Aries logo -

-

Aries Framework JavaScript Open ID Connect For Verifiable Credentials Client Module

-

- License - typescript - @aries-framework/openid4vc-verifier version - -

-
- -Open ID Connect For Verifiable Credentials Verifier Module for [Aries Framework JavaScript](https://github.com/hyperledger/aries-framework-javascript). - -### Installation - -Make sure you have set up the correct version of Aries Framework JavaScript according to the AFJ repository. - -```sh -yarn add @aries-framework/openid4vc-verifier -``` - -### Quick start - -#### Requirements - -#### Module registration - -In order to get this module to work, we need to inject it into the agent. This makes the module's functionality accessible through the agent's `modules` api. - -```ts -import { OpenId4VcVerifierModule } from '@aries-framework/openid4vc-verifier' - -const agent = new Agent({ - config: { - /* config */ - }, - dependencies: agentDependencies, - modules: { - openId4VcVerifier: new OpenId4VcVerifierModule(), - /* other custom modules */ - }, -}) - -await agent.initialize() -``` - -How the module is injected and the agent has been initialized, you can access the module's functionality through `agent.modules.openId4VcVerifier`. - -#### Preparing a DID diff --git a/packages/openid4vc/src/openid4vc-verifier/__tests__/openId4vc-verifier-module.test.ts b/packages/openid4vc/src/openid4vc-verifier/__tests__/openId4vc-verifier-module.test.ts index 699011880b..c45cb612ec 100644 --- a/packages/openid4vc/src/openid4vc-verifier/__tests__/openId4vc-verifier-module.test.ts +++ b/packages/openid4vc/src/openid4vc-verifier/__tests__/openId4vc-verifier-module.test.ts @@ -14,7 +14,12 @@ const dependencyManager = { describe('OpenId4VcVerifierModule', () => { test('registers dependencies on the dependency manager', () => { - const openId4VcClientModule = new OpenId4VcVerifierModule({}) + const verifierMetadata = { + verifierBaseUrl: 'http://redirect-uri', + verificationEndpointPath: '', + } + const openId4VcClientModule = new OpenId4VcVerifierModule({ verifierMetadata }) + openId4VcClientModule.register(dependencyManager) expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) diff --git a/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.e2e.test.ts b/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.e2e.test.ts index 67c08da693..d6ce60356e 100644 --- a/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.e2e.test.ts @@ -1,90 +1,39 @@ -import type { HolderMetadata, PresentationDefinitionV2 } from '..' -import type { KeyDidCreateOptions, VerificationMethod } from '@aries-framework/core' - import { AskarModule } from '@aries-framework/askar' -import { Agent, DidKey, Jwt, KeyType, TypedArrayEncoder } from '@aries-framework/core' -import { agentDependencies } from '@aries-framework/node' +import { Jwt } from '@aries-framework/core' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { SigningAlgo } from '@sphereon/did-auth-siop' import { cleanAll, enableNetConnect } from 'nock' -import { OpenId4VcVerifierModule, staticOpOpenIdConfig, staticOpSiopConfig } from '..' +import { OpenId4VcVerifierModule } from '..' +import { createAgentFromModules, type AgentType } from '../../../tests/utils' +import { + staticOpOpenIdConfigEdDSA, + staticSiopConfigEDDSA, + universityDegreePresentationDefinition, +} from '../../../tests/utilsVp' const modules = { - openId4VcVerifier: new OpenId4VcVerifierModule({}), + openId4VcVerifier: new OpenId4VcVerifierModule({ + verifierMetadata: { + verifierBaseUrl: 'http://redirect-uri', + verificationEndpointPath: '', + }, + }), askar: new AskarModule({ ariesAskar, }), } -export const staticSiopConfigEDDSA: HolderMetadata = { - ...staticOpSiopConfig, - idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA], - requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA], - vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] }, jwt_vp: { alg: [SigningAlgo.EDDSA] } }, -} - -export const staticOpOpenIdConfigEDDSA: HolderMetadata = { - ...staticOpOpenIdConfig, - idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA], - requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA], - vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] }, jwt_vp: { alg: [SigningAlgo.EDDSA] } }, -} - -const universityDegreePresentationDefinition: PresentationDefinitionV2 = { - id: 'UniversityDegreeCredential', - input_descriptors: [ - { - id: 'UniversityDegree', - // changed jwt_vc_json to jwt_vc - format: { jwt_vc: { alg: ['EdDSA'] } }, - // changed $.type to $.vc.type - constraints: { - fields: [{ path: ['$.vc.type.*'], filter: { type: 'string', pattern: 'UniversityDegree' } }], - }, - }, - ], -} - describe('OpenId4VcVerifier', () => { - let agent: Agent - let did: string - let kid: string - let verificationMethod: VerificationMethod + let verifier: AgentType beforeEach(async () => { - agent = new Agent({ - config: { - label: 'OpenId4VcVerifier Test', - walletConfig: { - id: 'openid4vc-Verifier-test', - key: 'openid4vc-Verifier-test', - }, - }, - dependencies: agentDependencies, - modules, - }) - - await agent.initialize() - - const _did = await agent.dids.create({ - method: 'key', - options: { keyType: KeyType.Ed25519 }, - secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598f') }, - }) - - did = _did.didState.did as string - - const didKey = DidKey.fromDid(did) - kid = `${did}#${didKey.key.fingerprint}` - const _verificationMethod = _did.didState.didDocument?.dereferenceKey(kid, ['authentication']) - if (!_verificationMethod) throw new Error('No verification method found') - verificationMethod = _verificationMethod + verifier = await createAgentFromModules('verifier', { ...modules }, '96213c3d7fc8d4d6754c7a0fd969598f') }) afterEach(async () => { - await agent.shutdown() - await agent.wallet.delete() + await verifier.agent.shutdown() + await verifier.agent.wallet.delete() }) describe('Verification', () => { @@ -95,18 +44,18 @@ describe('OpenId4VcVerifier', () => { it(`cannot sign authorization request with alg that isn't supported by the OpenId Provider`, async () => { await expect( - agent.modules.openId4VcVerifier.createProofRequest({ - redirectUri: 'http://redirect-uri', - verificationMethod, + verifier.agent.modules.openId4VcVerifier.createProofRequest({ + verificationEndpointUrl: 'http://redirect-uri', + verificationMethod: verifier.verificationMethod, }) ).rejects.toThrow() }) it(`check openid proof request format`, async () => { - const { proofRequest } = await agent.modules.openId4VcVerifier.createProofRequest({ - redirectUri: 'http://redirect-uri', - verificationMethod, - holderMetadata: staticOpOpenIdConfigEDDSA, + const { proofRequest } = await verifier.agent.modules.openId4VcVerifier.createProofRequest({ + verificationEndpointUrl: 'http://redirect-uri', + verificationMethod: verifier.verificationMethod, + holderMetadata: staticOpOpenIdConfigEdDSA, presentationDefinition: universityDegreePresentationDefinition, }) @@ -119,24 +68,24 @@ describe('OpenId4VcVerifier', () => { expect(proofRequest.startsWith(base)).toBe(true) - expect(jwt.header.kid).toEqual(kid) + expect(jwt.header.kid).toEqual(verifier.kid) expect(jwt.header.alg).toEqual(SigningAlgo.EDDSA) expect(jwt.header.typ).toEqual('JWT') expect(jwt.payload.additionalClaims.scope).toEqual('openid') - expect(jwt.payload.additionalClaims.client_id).toEqual(kid) + expect(jwt.payload.additionalClaims.client_id).toEqual(verifier.kid) expect(jwt.payload.additionalClaims.redirect_uri).toEqual('http://redirect-uri') expect(jwt.payload.additionalClaims.response_mode).toEqual('post') expect(jwt.payload.additionalClaims.nonce).toBeDefined() expect(jwt.payload.additionalClaims.state).toBeDefined() expect(jwt.payload.additionalClaims.response_type).toEqual('id_token vp_token') - expect(jwt.payload.iss).toEqual(did) - expect(jwt.payload.sub).toEqual(did) + expect(jwt.payload.iss).toEqual(verifier.did) + expect(jwt.payload.sub).toEqual(verifier.did) }) it(`check siop proof request format`, async () => { - const { proofRequest } = await agent.modules.openId4VcVerifier.createProofRequest({ - redirectUri: 'http://redirect-uri', - verificationMethod, + const { proofRequest } = await verifier.agent.modules.openId4VcVerifier.createProofRequest({ + verificationEndpointUrl: 'http://redirect-uri', + verificationMethod: verifier.verificationMethod, holderMetadata: staticSiopConfigEDDSA, }) @@ -147,17 +96,17 @@ describe('OpenId4VcVerifier', () => { const _jwt = proofRequest.substring(base.length) const jwt = Jwt.fromSerializedJwt(_jwt) - expect(jwt.header.kid).toEqual(kid) + expect(jwt.header.kid).toEqual(verifier.kid) expect(jwt.header.alg).toEqual(SigningAlgo.EDDSA) expect(jwt.payload.additionalClaims.scope).toEqual('openid') - expect(jwt.payload.additionalClaims.client_id).toEqual(kid) + expect(jwt.payload.additionalClaims.client_id).toEqual(verifier.kid) expect(jwt.payload.additionalClaims.redirect_uri).toEqual('http://redirect-uri') expect(jwt.payload.additionalClaims.response_mode).toEqual('post') expect(jwt.payload.additionalClaims.response_type).toEqual('id_token') expect(jwt.payload.additionalClaims.nonce).toBeDefined() expect(jwt.payload.additionalClaims.state).toBeDefined() - expect(jwt.payload.iss).toEqual(did) - expect(jwt.payload.sub).toEqual(did) + expect(jwt.payload.iss).toEqual(verifier.did) + expect(jwt.payload.sub).toEqual(verifier.did) }) }) }) diff --git a/packages/openid4vc/src/openid4vc-verifier/__tests__/setup.ts b/packages/openid4vc/src/openid4vc-verifier/__tests__/setup.ts deleted file mode 100644 index 34e38c9705..0000000000 --- a/packages/openid4vc/src/openid4vc-verifier/__tests__/setup.ts +++ /dev/null @@ -1 +0,0 @@ -jest.setTimeout(120000) diff --git a/packages/openid4vc/src/openid4vc-verifier/router/OpenId4VpEndpointConfiguration.ts b/packages/openid4vc/src/openid4vc-verifier/router/OpenId4VpEndpointConfiguration.ts new file mode 100644 index 0000000000..1d1bc95d14 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/router/OpenId4VpEndpointConfiguration.ts @@ -0,0 +1,47 @@ +import type { OpenId4VcVerifierService } from '../OpenId4VcVerifierService' +import type { VerificationEndpointConfig } from '../OpenId4VcVerifierServiceOptions' +import type { AgentContext, Logger } from '@aries-framework/core' +import type { AuthorizationResponsePayload } from '@sphereon/did-auth-siop' +import type { Router, Request } from 'express' + +import { getRequestContext, sendErrorResponse } from '../../shared/router' + +export interface VerificationRequestContext { + agentContext: AgentContext + openId4VcVerifierService: OpenId4VcVerifierService + logger: Logger +} + +export interface VerificationRequest extends Request { + requestContext?: VerificationRequestContext +} + +export type InternalVerificationEndpointConfig = VerificationEndpointConfig + +export const configureVerificationEndpoint = ( + router: Router, + pathname: string, + config: InternalVerificationEndpointConfig +) => { + router.post(pathname, async (request: VerificationRequest, response) => { + const { logger, agentContext, openId4VcVerifierService } = getRequestContext(request) + try { + const isVpRequest = request.body.presentation_submission !== undefined + + const authorizationResponse: AuthorizationResponsePayload = request.body + if (isVpRequest) authorizationResponse.presentation_submission = JSON.parse(request.body.presentation_submission) + + const verifiedProofResponse = await openId4VcVerifierService.verifyProofResponse(agentContext, request.body) + if (!config.proofResponseHandler) return response.status(200).send() + + const { status } = await config.proofResponseHandler(verifiedProofResponse) + return response.status(status).send() + } catch (error: unknown) { + sendErrorResponse(response, logger, 500, 'invalid_request', error) + } + + return response.status(200).send() + }) + + return router +} diff --git a/packages/openid4vc/src/shared/router.ts b/packages/openid4vc/src/shared/router.ts new file mode 100644 index 0000000000..374862fb8f --- /dev/null +++ b/packages/openid4vc/src/shared/router.ts @@ -0,0 +1,59 @@ +import type { AgentContextProvider, Logger } from '@aries-framework/core' +import type { Response, Request } from 'express' + +import { AriesFrameworkError, WalletApi } from '@aries-framework/core' +import path from 'path' + +export function sendErrorResponse(response: Response, logger: Logger, code: number, message: string, error: unknown) { + const error_description = + error instanceof Error ? error.message : typeof error === 'string' ? error : 'An unknown error occurred.' + + const body = { error: message, error_description } + logger.warn(`[OID4VCI] Sending error response: ${JSON.stringify(body)}`) + + return response.status(code).json(body) +} + +export function getRequestContext( + request: T +): NonNullable { + const requestContext = request.requestContext + if (!requestContext) throw new AriesFrameworkError('Request context not set.') + + return requestContext +} + +export async function initializeAgentFromContext(contextCorrelationId: string, contextProvider: AgentContextProvider) { + const agentContext = await contextProvider.getAgentContextForContextCorrelationId(contextCorrelationId) + + const { walletConfig } = agentContext.config + + const walletApi = agentContext.dependencyManager.resolve(WalletApi) + if (!walletApi.isInitialized && walletConfig) { + await walletApi.initialize(walletConfig) + } + + return agentContext +} + +export function getEndpointUrl(base: string, basePath: string, endpoint = '/') { + const baseUrl = new URL(base) + + if (URL.canParse(endpoint)) throw new AriesFrameworkError(`Endpoint must be relative not absolute: '${endpoint}'`) + + // if the endpoint is relative, append it to origin of the base + // if the endpoint is absolute, use the pathname + const endpointPath = path.join(basePath, endpoint) + const url = + endpointPath === '' || endpointPath === '/' || endpointPath === '.' ? baseUrl : new URL(endpointPath, baseUrl) + + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + throw new AriesFrameworkError(`Endpoint '${endpoint}' is not valid. Invalid protocol '${url.protocol}'`) + } + + if (url.origin !== baseUrl.origin) { + throw new AriesFrameworkError(`Endpoint '${endpoint}' is not valid. Invalid origin '${url}'`) + } + + return url.href.replace(/\/$/, '') +} diff --git a/packages/openid4vc/src/openid4vc-holder/presentation/transform.ts b/packages/openid4vc/src/shared/transform.ts similarity index 76% rename from packages/openid4vc/src/openid4vc-holder/presentation/transform.ts rename to packages/openid4vc/src/shared/transform.ts index edb837d66c..bd7ffa9648 100644 --- a/packages/openid4vc/src/openid4vc-holder/presentation/transform.ts +++ b/packages/openid4vc/src/shared/transform.ts @@ -1,6 +1,7 @@ import type { W3cVerifiableCredential, W3cVerifiablePresentation } from '@aries-framework/core' import type { - OriginalVerifiableCredential as SphereonW3cVerifiableCredential, + OriginalVerifiableCredential as SphereonOriginalVerifiableCredential, + W3CVerifiableCredential as SphereonW3cVerifiableCredential, W3CVerifiablePresentation as SphereonW3cVerifiablePresentation, } from '@sphereon/ssi-types' @@ -14,6 +15,20 @@ import { W3cJsonLdVerifiableCredential, } from '@aries-framework/core' +export function getSphereonOriginalVerifiableCredential( + w3cVerifiableCredential: W3cVerifiableCredential +): SphereonOriginalVerifiableCredential { + if (w3cVerifiableCredential.claimFormat === ClaimFormat.LdpVc) { + return JsonTransformer.toJSON(w3cVerifiableCredential) as SphereonOriginalVerifiableCredential + } else if (w3cVerifiableCredential.claimFormat === ClaimFormat.JwtVc) { + return w3cVerifiableCredential.serializedJwt + } else { + throw new AriesFrameworkError( + `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` + ) + } +} + export function getSphereonW3cVerifiableCredential( w3cVerifiableCredential: W3cVerifiableCredential ): SphereonW3cVerifiableCredential { diff --git a/packages/openid4vc/src/openid4vc-verifier/utils.ts b/packages/openid4vc/src/shared/utils.ts similarity index 79% rename from packages/openid4vc/src/openid4vc-verifier/utils.ts rename to packages/openid4vc/src/shared/utils.ts index c514f10dbe..c23d030178 100644 --- a/packages/openid4vc/src/openid4vc-verifier/utils.ts +++ b/packages/openid4vc/src/shared/utils.ts @@ -7,6 +7,7 @@ import { TypedArrayEncoder, getKeyFromVerificationMethod, getJwkClassFromKeyType, + SignatureSuiteRegistry, } from '@aries-framework/core' /** @@ -84,3 +85,24 @@ export function getResolver(agentContext: AgentContext) { }, } } + +export async function generateRandomValues(agentContext: AgentContext, count: number) { + const randomValuesPromises = Array.from({ length: count }, () => agentContext.wallet.generateNonce()) + return await Promise.all(randomValuesPromises) +} + +export const getProofTypeFromVerificationMethod = ( + agentContext: AgentContext, + verificationMethod: VerificationMethod +) => { + const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) + + const supportedSignatureSuite = signatureSuiteRegistry.getByVerificationMethodType(verificationMethod.type) + if (!supportedSignatureSuite) { + throw new AriesFrameworkError( + `Couldn't find a supported signature suite for the given verification method type '${verificationMethod.type}'.` + ) + } + + return supportedSignatureSuite.proofType +} diff --git a/packages/openid4vc/tests/openid4vc.e2e.test.ts b/packages/openid4vc/tests/openid4vc.e2e.test.ts new file mode 100644 index 0000000000..6e16cc135b --- /dev/null +++ b/packages/openid4vc/tests/openid4vc.e2e.test.ts @@ -0,0 +1,465 @@ +import type { IssuerMetadata } from './../src/openid4vc-issuer' +import type { AgentType, TenantType } from './utils' +import type { CreateProofRequestOptions } from '../src' +import type { Server } from 'http' + +import { AskarModule } from '@aries-framework/askar' +import { + ClaimFormat, + JwaSignatureAlgorithm, + W3cCredential, + W3cCredentialService, + W3cCredentialSubject, + W3cIssuer, + w3cDate, +} from '@aries-framework/core' +import { SdJwtVcModule } from '@aries-framework/sd-jwt-vc' +import { TenantsModule } from '@aries-framework/tenants' +import { ariesAskar } from '@hyperledger/aries-askar-nodejs' +import express, { Router, type Express } from 'express' + +import { SdJwtCredential } from '../../sd-jwt-vc/src/SdJwtCredential' +import { OpenId4VcVerifierModule } from '../src' +import { OpenId4VcHolderModule } from '../src/openid4vc-holder' +import { OpenId4VcIssuerModule } from '../src/openid4vc-issuer' + +import { createAgentFromModules, createTenantForAgent } from './utils' +import { allCredentialsSupported, universityDegreeCredentialSdJwt, universityDegreeCredentialSdJwt2 } from './utilsVci' +import { + openBadgePresentationDefinition, + staticOpOpenIdConfigEdDSA, + universityDegreePresentationDefinition, + waitForMockFunction, +} from './utilsVp' + +const issuerPort = 1234 +const baseUrl = `http://localhost:${issuerPort}` + +const baseCredentialRequestOptions = { + scheme: 'openid-credential-offer', + baseUri: baseUrl, +} + +const issuerMetadata: IssuerMetadata = { + issuerBaseUrl: baseUrl, + credentialEndpointPath: `/credentials`, + tokenEndpointPath: `/token`, + credentialsSupported: allCredentialsSupported, +} +const holderModules = { + openId4VcHolder: new OpenId4VcHolderModule(), + sdJwtVc: new SdJwtVcModule(), + askar: new AskarModule({ ariesAskar }), +} as const + +const issuerModules = { + openId4VcIssuer: new OpenId4VcIssuerModule({ issuerMetadata }), + sdJwtVc: new SdJwtVcModule(), + askar: new AskarModule({ ariesAskar }), +} as const + +const verifierModules = { + openId4VcVerifier: new OpenId4VcVerifierModule({ + verifierMetadata: { + verifierBaseUrl: baseUrl, + verificationEndpointPath: '/verify', + }, + }), + sdJwtVc: new SdJwtVcModule(), + askar: new AskarModule({ ariesAskar }), +} as const + +describe('OpenId4Vc', () => { + let expressApp: Express + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let expressServer: Server + + let issuer: AgentType }> + let issuer1: TenantType + let issuer2: TenantType + + let holder: AgentType }> + let holder1: TenantType + + let verifier: AgentType }> + let verifier1: TenantType + let verifier2: TenantType + + beforeEach(async () => { + expressApp = express() + + issuer = await createAgentFromModules( + 'issuer', + { ...issuerModules, tenants: new TenantsModule() }, + '96213c3d7fc8d4d6754c7a0fd969598g' + ) + issuer1 = await createTenantForAgent(issuer.agent as any, 'iTenant1') + issuer2 = await createTenantForAgent(issuer.agent as any, 'iTenant2') + + holder = await createAgentFromModules( + 'holder', + { ...holderModules, tenants: new TenantsModule() }, + '96213c3d7fc8d4d6754c7a0fd969598e' + ) + holder1 = await createTenantForAgent(holder.agent as any, 'hTenant1') + + verifier = await createAgentFromModules( + 'verifier', + { ...verifierModules, tenants: new TenantsModule() }, + '96213c3d7fc8d4d6754c7a0fd969598f' + ) + verifier1 = await createTenantForAgent(verifier.agent as any, 'vTenant1') + verifier2 = await createTenantForAgent(verifier.agent as any, 'vTenant2') + }) + + afterEach(async () => { + expressServer?.close() + + await issuer.agent.shutdown() + await issuer.agent.wallet.delete() + + await holder.agent.shutdown() + await holder.agent.wallet.delete() + }) + + it('e2e flow with tenants, issuer endpoints requesting a sdjwtvc', async () => { + const issuerTenant1 = await issuer.agent.modules.tenants.getTenantAgent({ tenantId: issuer1.tenantId }) + const issuer1Router = Router() + const issuer1BasePath = '/issuer1' + + await issuerTenant1.modules.openId4VcIssuer.configureRouter(issuer1Router, { + basePath: issuer1BasePath, + metadataEndpointConfig: { enabled: true }, + accessTokenEndpointConfig: { + enabled: true, + preAuthorizedCodeExpirationDuration: 50, + verificationMethod: issuer1.verificationMethod, + }, + credentialEndpointConfig: { + enabled: true, + verificationMethod: issuer1.verificationMethod, + credentialRequestToCredentialMapper: async ({ credentialRequest, holderDid, holderDidUrl }) => { + if ( + credentialRequest.format === 'vc+sd-jwt' && + credentialRequest.credential_definition.vct === 'UniversityDegreeCredential' + ) { + if (holderDid !== holder1.did) throw new Error('Invalid holder did') + + return new SdJwtCredential({ + payload: { type: 'UniversityDegreeCredential', university: 'innsbruck', degree: 'bachelor' }, + holderDidUrl: holderDidUrl, + issuerDidUrl: issuer1.kid, + disclosureFrame: { university: true, degree: true }, + }) + } + + throw new Error('Invalid request') + }, + }, + }) + + const issuerTenant2 = await issuer.agent.modules.tenants.getTenantAgent({ tenantId: issuer2.tenantId }) + const issuer2Router = Router() + const issuer2BasePath = '/issuer2' + + await issuerTenant2.modules.openId4VcIssuer.configureRouter(issuer2Router, { + basePath: issuer2BasePath, + metadataEndpointConfig: { enabled: true }, + accessTokenEndpointConfig: { + enabled: true, + preAuthorizedCodeExpirationDuration: 50, + verificationMethod: issuer2.verificationMethod, + }, + credentialEndpointConfig: { + enabled: true, + verificationMethod: issuer2.verificationMethod, + credentialRequestToCredentialMapper: async ({ credentialRequest, holderDid, holderDidUrl }) => { + if ( + credentialRequest.format === 'vc+sd-jwt' && + credentialRequest.credential_definition.vct === 'UniversityDegreeCredential2' + ) { + if (holderDid !== holder1.did) throw new Error('Invalid holder did') + + return new SdJwtCredential({ + payload: { type: 'UniversityDegreeCredential2', university: 'innsbruck', degree: 'bachelor' }, + holderDidUrl: holderDidUrl, + issuerDidUrl: issuer2.kid, + disclosureFrame: { university: true, degree: true }, + }) + } + + throw new Error('Invalid request') + }, + }, + }) + + expressApp.use(issuer1BasePath, issuer1Router) + expressApp.use(issuer2BasePath, issuer2Router) + expressServer = expressApp.listen(issuerPort) + + const { credentialOfferRequest: credentialOfferRequest1 } = + await issuerTenant1.modules.openId4VcIssuer.createCredentialOfferAndRequest( + [universityDegreeCredentialSdJwt.id], + { preAuthorizedCodeFlowConfig: { userPinRequired: false }, ...baseCredentialRequestOptions } + ) + + const { credentialOfferRequest: credentialOfferRequest2 } = + await issuerTenant2.modules.openId4VcIssuer.createCredentialOfferAndRequest( + [universityDegreeCredentialSdJwt2.id], + { preAuthorizedCodeFlowConfig: { userPinRequired: false }, ...baseCredentialRequestOptions } + ) + + await issuerTenant1.endSession() + await issuerTenant2.endSession() + + const holderTenant1 = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId }) + + const resolvedCredentialOffer1 = await holderTenant1.modules.openId4VcHolder.resolveCredentialOffer( + credentialOfferRequest1 + ) + + expect(resolvedCredentialOffer1.credentialOfferPayload.credential_issuer).toEqual(`${baseUrl}/issuer1`) + expect(resolvedCredentialOffer1.metadata.credentialIssuerMetadata?.token_endpoint).toEqual( + `${baseUrl}/issuer1/token` + ) + expect(resolvedCredentialOffer1.metadata.credentialIssuerMetadata?.credential_endpoint).toEqual( + `${baseUrl}/issuer1/credentials` + ) + + const credentials1 = await holderTenant1.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + resolvedCredentialOffer1, + { + proofOfPossessionVerificationMethodResolver: async () => { + return holder1.verificationMethod + }, + } + ) + + const resolvedCredentialOffer2 = await holderTenant1.modules.openId4VcHolder.resolveCredentialOffer( + credentialOfferRequest2 + ) + expect(resolvedCredentialOffer2.credentialOfferPayload.credential_issuer).toEqual(`${baseUrl}/issuer2`) + expect(resolvedCredentialOffer2.metadata.credentialIssuerMetadata?.token_endpoint).toEqual( + `${baseUrl}/issuer2/token` + ) + expect(resolvedCredentialOffer2.metadata.credentialIssuerMetadata?.credential_endpoint).toEqual( + `${baseUrl}/issuer2/credentials` + ) + + expect(credentials1).toHaveLength(1) + if (credentials1[0].type === 'W3cCredentialRecord') throw new Error('Invalid credential type') + expect(credentials1[0].sdJwtVc.payload['type']).toEqual('UniversityDegreeCredential') + + const credentials2 = await holderTenant1.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + resolvedCredentialOffer2, + { + proofOfPossessionVerificationMethodResolver: async () => { + return holder1.verificationMethod + }, + } + ) + + expect(credentials2).toHaveLength(1) + if (credentials2[0].type === 'W3cCredentialRecord') throw new Error('Invalid credential type') + expect(credentials2[0].sdJwtVc.payload['type']).toEqual('UniversityDegreeCredential2') + + await holderTenant1.endSession() + }) + + it('e2e flow with tenants, verifier endpoints verifying a sdjwtvc', async () => { + const mockFunction1 = jest.fn() + mockFunction1.mockReturnValue({ status: 200 }) + + const mockFunction2 = jest.fn() + mockFunction2.mockReturnValue({ status: 200 }) + + const issuerTenant1 = await issuer.agent.modules.tenants.getTenantAgent({ tenantId: issuer1.tenantId }) + const holderTenant1 = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId }) + const verifierTenant1_1 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) + const verifierTenant2_1 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier2.tenantId }) + const verifier1Router = Router() + const verifier2Router = Router() + const verifier1BasePath = '/verifier1' + const verifier2BasePath = '/verifier2' + + const credential1 = new W3cCredential({ + type: ['VerifiableCredential', 'OpenBadgeCredential'], + issuer: new W3cIssuer({ id: issuer1.did }), + credentialSubject: new W3cCredentialSubject({ id: holder1.did }), + issuanceDate: w3cDate(Date.now()), + }) + + const credential2 = new W3cCredential({ + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: new W3cIssuer({ id: issuer1.did }), + credentialSubject: new W3cCredentialSubject({ id: holder1.did }), + issuanceDate: w3cDate(Date.now()), + }) + + const issuer1W3cCredentialService = issuerTenant1.dependencyManager.resolve(W3cCredentialService) + + const signed1 = await issuer1W3cCredentialService.signCredential(issuerTenant1.context, { + format: ClaimFormat.JwtVc, + credential: credential1, + alg: JwaSignatureAlgorithm.EdDSA, + verificationMethod: issuer1.verificationMethod.id, + }) + + const signed2 = await issuer1W3cCredentialService.signCredential(issuerTenant1.context, { + format: ClaimFormat.JwtVc, + credential: credential2, + alg: JwaSignatureAlgorithm.EdDSA, + verificationMethod: issuer1.verificationMethod.id, + }) + + await holderTenant1.w3cCredentials.storeCredential({ credential: signed1 }) + await holderTenant1.w3cCredentials.storeCredential({ credential: signed2 }) + + await verifierTenant1_1.modules.openId4VcVerifier.configureRouter(verifier1Router, { + basePath: '/verifier1', + verificationEndpointConfig: { + enabled: true, + proofResponseHandler: mockFunction1, + }, + }) + + await verifierTenant2_1.modules.openId4VcVerifier.configureRouter(verifier2Router, { + basePath: '/verifier2', + verificationEndpointConfig: { + enabled: true, + proofResponseHandler: mockFunction2, + }, + }) + + expressApp.use(verifier1BasePath, verifier1Router) + expressApp.use(verifier2BasePath, verifier2Router) + expressServer = expressApp.listen(issuerPort) + + const createProofRequestOptions1: CreateProofRequestOptions = { + verificationMethod: verifier1.verificationMethod, + holderMetadata: staticOpOpenIdConfigEdDSA, + presentationDefinition: openBadgePresentationDefinition, + } + + const createProofRequestOptions2: CreateProofRequestOptions = { + verificationMethod: verifier2.verificationMethod, + holderMetadata: staticOpOpenIdConfigEdDSA, + presentationDefinition: universityDegreePresentationDefinition, + } + + const { proofRequest: proofRequest1, proofRequestMetadata: proofRequestMetadata1 } = + await verifierTenant1_1.modules.openId4VcVerifier.createProofRequest(createProofRequestOptions1) + + expect( + proofRequest1.startsWith( + `openid://?redirect_uri=http%3A%2F%2Flocalhost%3A1234%2Fverifier1%2Fverify&presentation_definition=%7B%22id%22%3A%22OpenBadgeCredential` + ) + ).toBeTruthy() + + const { proofRequest: proofRequest2, proofRequestMetadata: proofRequestMetadata2 } = + await verifierTenant2_1.modules.openId4VcVerifier.createProofRequest(createProofRequestOptions2) + + expect( + proofRequest2.startsWith( + `openid://?redirect_uri=http%3A%2F%2Flocalhost%3A1234%2Fverifier2%2Fverify&presentation_definition=%7B%22id%22%3A%22UniversityDegreeCredential` + ) + ).toBeTruthy() + + await verifierTenant1_1.endSession() + await verifierTenant2_1.endSession() + + const result1 = await holderTenant1.modules.openId4VcHolder.resolveProofRequest(proofRequest1) + if (result1.proofType === 'authentication') throw new Error('Expected a proofRequest') + + result1.presentationSubmission.requirements[0] + + if (!result1.presentationSubmission.areRequirementsSatisfied) { + throw new Error('Requirements are not satisfied.') + } + + expect( + result1.presentationSubmission.requirements[0].submissionEntry[0].verifiableCredentials[0].credential.type + ).toContain('OpenBadgeCredential') + + const result2 = await holderTenant1.modules.openId4VcHolder.resolveProofRequest(proofRequest2) + if (result2.proofType === 'authentication') throw new Error('Expected a proofRequest') + + result2.presentationSubmission.requirements[0] + + if (!result2.presentationSubmission.areRequirementsSatisfied) { + throw new Error('Requirements are not satisfied.') + } + + expect( + result2.presentationSubmission.requirements[0].submissionEntry[0].verifiableCredentials[0].credential.type + ).toContain('UniversityDegreeCredential') + + const { status: status1, submittedResponse: submittedResponse1 } = + await holderTenant1.modules.openId4VcHolder.acceptPresentationRequest(result1.presentationRequest, { + submission: result1.presentationSubmission, + submissionEntryIndexes: [0], + }) + expect(status1).toBe(200) + + // The RP MUST validate that the aud (audience) Claim contains the value of the client_id + // that the RP sent in the Authorization Request as an audience. + // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. + const verifierTenant1_2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) + const { idTokenPayload: idTokenPayload1, submission: submission1 } = + await verifierTenant1_2.modules.openId4VcVerifier.verifyProofResponse(submittedResponse1) + + const { state: state1, challenge: challenge1 } = proofRequestMetadata1 + expect(idTokenPayload1).toBeDefined() + expect(idTokenPayload1.state).toMatch(state1) + expect(idTokenPayload1.nonce).toMatch(challenge1) + + expect(submission1).toBeDefined() + expect(submission1?.presentationDefinitions).toHaveLength(1) + expect(submission1?.submissionData.definition_id).toBe('OpenBadgeCredential') + expect(submission1?.presentations).toHaveLength(1) + expect(submission1?.presentations[0].vcs[0].credential.type).toEqual([ + 'VerifiableCredential', + 'OpenBadgeCredential', + ]) + + await waitForMockFunction(mockFunction1) + expect(mockFunction1).toBeCalledWith({ + idTokenPayload: expect.objectContaining(idTokenPayload1), + submission: expect.objectContaining(submission1), + }) + + const { status: status2, submittedResponse: submittedResponse2 } = + await holderTenant1.modules.openId4VcHolder.acceptPresentationRequest(result2.presentationRequest, { + submission: result2.presentationSubmission, + submissionEntryIndexes: [0], + }) + expect(status2).toBe(200) + + // The RP MUST validate that the aud (audience) Claim contains the value of the client_id + // that the RP sent in the Authorization Request as an audience. + // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. + const verifierTenant2_2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier2.tenantId }) + const { idTokenPayload: idTokenPayload2, submission: submission2 } = + await verifierTenant2_2.modules.openId4VcVerifier.verifyProofResponse(submittedResponse2) + + const { state: state2, challenge: challenge2 } = proofRequestMetadata2 + expect(idTokenPayload2).toBeDefined() + expect(idTokenPayload2.state).toMatch(state2) + expect(idTokenPayload2.nonce).toMatch(challenge2) + + expect(submission2).toBeDefined() + expect(submission2?.presentationDefinitions).toHaveLength(1) + expect(submission2?.submissionData.definition_id).toBe('UniversityDegreeCredential') + expect(submission2?.presentations).toHaveLength(1) + expect(submission2?.presentations[0].vcs[0].credential.type).toEqual([ + 'VerifiableCredential', + 'UniversityDegreeCredential', + ]) + + await waitForMockFunction(mockFunction2) + expect(mockFunction2).toBeCalledWith({ + idTokenPayload: expect.objectContaining(idTokenPayload2), + submission: expect.objectContaining(submission2), + }) + }) +}) diff --git a/packages/openid4vc/tests/utils.ts b/packages/openid4vc/tests/utils.ts new file mode 100644 index 0000000000..b00b02e7e7 --- /dev/null +++ b/packages/openid4vc/tests/utils.ts @@ -0,0 +1,71 @@ +import type { KeyDidCreateOptions, ModulesMap } from '@aries-framework/core' +import type { TenantsModule } from '@aries-framework/tenants' +import type { TenantAgent } from '@aries-framework/tenants/src/TenantAgent' + +import { Agent, DidKey, KeyType, TypedArrayEncoder, utils } from '@aries-framework/core' +import { agentDependencies } from '@aries-framework/node' + +export async function createDidKidVerificationMethod(agent: Agent | TenantAgent, secretKey: string) { + const didCreateResult = await agent.dids.create({ + method: 'key', + options: { keyType: KeyType.Ed25519 }, + secret: { privateKey: TypedArrayEncoder.fromString(secretKey) }, + }) + + const did = didCreateResult.didState.did as string + const didKey = DidKey.fromDid(did) + const kid = `${did}#${didKey.key.fingerprint}` + + const verificationMethod = didCreateResult.didState.didDocument?.dereferenceKey(kid, ['authentication']) + if (!verificationMethod) throw new Error('No verification method found') + + return { + did, + kid, + verificationMethod, + } +} + +export async function createAgentFromModules(label: string, modulesMap: MM, secretKey: string) { + const agent = new Agent({ + config: { label, walletConfig: { id: utils.uuid(), key: utils.uuid() } }, + dependencies: agentDependencies, + modules: modulesMap, + }) + + await agent.initialize() + const data = await createDidKidVerificationMethod(agent, secretKey) + + return { + ...data, + agent, + } +} + +export type AgentType = Awaited>> + +export async function createTenantForAgent( + agent: Agent<{ tenants: TenantsModule }>, + label: string +) { + const tenantRecord = await agent.modules.tenants.createTenant({ + config: { + label, + }, + }) + + const nonce1 = await agent.wallet.generateNonce() + const nonce2 = await agent.wallet.generateNonce() + const secretKey = (nonce1 + nonce2).slice(0, 32) + + const tenant = await agent.modules.tenants.getTenantAgent({ tenantId: tenantRecord.id }) + const data = await createDidKidVerificationMethod(tenant as any, secretKey) + await tenant.endSession() + + return { + ...data, + tenantId: tenantRecord.id, + } +} + +export type TenantType = Awaited>> diff --git a/packages/openid4vc/tests/utilsVci.ts b/packages/openid4vc/tests/utilsVci.ts new file mode 100644 index 0000000000..f17937f5a5 --- /dev/null +++ b/packages/openid4vc/tests/utilsVci.ts @@ -0,0 +1,46 @@ +import type { CredentialSupported } from '@sphereon/oid4vci-common' + +import { OpenIdCredentialFormatProfile } from '../src' + +export const openBadgeCredential: CredentialSupported & { id: string } = { + id: `/credentials/OpenBadgeCredential`, + format: OpenIdCredentialFormatProfile.JwtVcJson, + types: ['VerifiableCredential', 'OpenBadgeCredential'], +} + +export const universityDegreeCredential: CredentialSupported & { id: string } = { + id: `/credentials/UniversityDegreeCredential`, + format: OpenIdCredentialFormatProfile.JwtVcJson, + types: ['VerifiableCredential', 'UniversityDegreeCredential'], +} + +export const universityDegreeCredentialLd: CredentialSupported & { id: string } = { + id: `/credentials/UniversityDegreeCredentialLd`, + format: OpenIdCredentialFormatProfile.JwtVcJsonLd, + types: ['VerifiableCredential', 'UniversityDegreeCredential'], + '@context': ['context'], +} + +export const universityDegreeCredentialSdJwt = { + id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredentialSdJwt', + format: OpenIdCredentialFormatProfile.SdJwtVc, + credential_definition: { + vct: 'UniversityDegreeCredential', + }, +} satisfies CredentialSupported & { id: string } + +export const universityDegreeCredentialSdJwt2 = { + id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredentialSdJwt2', + format: OpenIdCredentialFormatProfile.SdJwtVc, + credential_definition: { + vct: 'UniversityDegreeCredential2', + }, +} satisfies CredentialSupported & { id: string } + +export const allCredentialsSupported = [ + openBadgeCredential, + universityDegreeCredential, + universityDegreeCredentialLd, + universityDegreeCredentialSdJwt, + universityDegreeCredentialSdJwt2, +] diff --git a/packages/openid4vc/tests/utilsVp.ts b/packages/openid4vc/tests/utilsVp.ts new file mode 100644 index 0000000000..768e469c3e --- /dev/null +++ b/packages/openid4vc/tests/utilsVp.ts @@ -0,0 +1,88 @@ +import type { HolderMetadata } from '../src' +import type { PresentationDefinitionV2 } from '@sphereon/pex-models' + +import { SigningAlgo } from '@sphereon/did-auth-siop' + +import { staticOpOpenIdConfig } from '../src' +import { staticOpSiopConfig } from '../src/openid4vc-verifier/OpenId4VcVerifierServiceOptions' +// id id%22%3A%22test%22%2C%22 +// * = %2A +// TODO: error on sphereon lib PR opened +// TODO: walt issued credentials verification fails due to some time issue || //throw new Error(`Inconsistent issuance dates between JWT claim (${nbfDateAsStr}) and VC value (${issuanceDate})`); +// TODO: error walt no id in presentation definition +// TODO: error walt vc.type is an array not a string thus the filter does not work $.type (should be array according to vc data 1.1) +// TODO: jwt_vc vs jwt_vc_json +export const waltPortalOpenBadgeJwt = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa3RpUVFFcW0yeWFwWEJEdDFXRVZCM2RxZ3Z5emk5NkZ1RkFOWW1yZ1RyS1Y5I3o2TWt0aVFRRXFtMnlhcFhCRHQxV0VWQjNkcWd2eXppOTZGdUZBTlltcmdUcktWOSJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiT3BlbkJhZGdlQ3JlZGVudGlhbCJdLCJjcmVkZW50aWFsU3ViamVjdCI6e319LCJpc3MiOiJkaWQ6a2V5Ono2TWt0aVFRRXFtMnlhcFhCRHQxV0VWQjNkcWd2eXppOTZGdUZBTlltcmdUcktWOSIsInN1YiI6ImRpZDprZXk6ejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjIiwibmJmIjoxNzAwNzQzMzM1fQ.OcKPyaWeVV-78BWr8N4h2Cyvjtc9jzknAqvTA77hTbKCNCEbhGboo-S6yXHLC-3NWYQ1vVcqZmdPlIOrHZ7MDw' + +export const waltUniversityDegreeJwt = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa3RpUVFFcW0yeWFwWEJEdDFXRVZCM2RxZ3Z5emk5NkZ1RkFOWW1yZ1RyS1Y5I3o2TWt0aVFRRXFtMnlhcFhCRHQxV0VWQjNkcWd2eXppOTZGdUZBTlltcmdUcktWOSJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVW5pdmVyc2l0eURlZ3JlZUNyZWRlbnRpYWwiXSwiY3JlZGVudGlhbFN1YmplY3QiOnt9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdGlRUUVxbTJ5YXBYQkR0MVdFVkIzZHFndnl6aTk2RnVGQU5ZbXJnVHJLVjkiLCJzdWIiOiJkaWQ6a2V5Ono2TWtwR1I0Z3M0UmMzWnBoNHZqOHdSbmpuQXhnQVBTeGNSOE1BVkt1dFdzcFF6YyIsIm5iZiI6MTcwMDc0MzM5NH0.EhMnE349oOvzbu0rFl-m_7FOoRsB5VucLV5tUUIW0jPxkJ7J0qVLOJTXVX4KNv_N9oeP8pgTUvydd6nxB_0KCQ' + +export const universityDegreePresentationDefinition: PresentationDefinitionV2 = { + id: 'UniversityDegreeCredential', + input_descriptors: [ + { + id: 'UniversityDegree', + // changed jwt_vc_json to jwt_vc + format: { jwt_vc: { alg: ['EdDSA'] } }, + // changed $.type to $.vc.type + constraints: { + fields: [{ path: ['$.vc.type.*'], filter: { type: 'string', pattern: 'UniversityDegree' } }], + }, + }, + ], +} + +export const openBadgePresentationDefinition: PresentationDefinitionV2 = { + id: 'OpenBadgeCredential', + input_descriptors: [ + { + id: 'OpenBadgeCredential', + // changed jwt_vc_json to jwt_vc + format: { jwt_vc: { alg: ['EdDSA'] } }, + // changed $.type to $.vc.type + constraints: { + fields: [{ path: ['$.vc.type.*'], filter: { type: 'string', pattern: 'OpenBadgeCredential' } }], + }, + }, + ], +} + +export const combinePresentationDefinitions = ( + presentationDefinitions: PresentationDefinitionV2[] +): PresentationDefinitionV2 => { + return { + id: 'Combined', + input_descriptors: presentationDefinitions.flatMap((p) => p.input_descriptors), + } +} + +export const staticOpOpenIdConfigEdDSA: HolderMetadata = { + ...staticOpOpenIdConfig, + idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA], + requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA], + vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] }, jwt_vp: { alg: [SigningAlgo.EDDSA] } }, +} + +export const staticSiopConfigEDDSA: HolderMetadata = { + ...staticOpSiopConfig, + idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA], + requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA], + vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] }, jwt_vp: { alg: [SigningAlgo.EDDSA] } }, +} + +export function waitForMockFunction(mockFn: jest.Mock) { + return new Promise((resolve, reject) => { + const intervalId = setInterval(() => { + if (mockFn.mock.calls.length > 0) { + clearInterval(intervalId) + resolve(0) + } + }, 100) + + setTimeout(() => { + clearInterval(intervalId) + reject(new Error('Timeout Callback')) + }, 10000) + }) +} diff --git a/packages/sd-jwt-vc/src/SdJwtCredential.ts b/packages/sd-jwt-vc/src/SdJwtCredential.ts new file mode 100644 index 0000000000..ac43aff8b8 --- /dev/null +++ b/packages/sd-jwt-vc/src/SdJwtCredential.ts @@ -0,0 +1,29 @@ +import type { HashName, JwaSignatureAlgorithm } from '@aries-framework/core' +import type { DisclosureFrame } from 'jwt-sd' + +export interface SdJwtCredentialOptions = Record> { + payload: Payload + holderDidUrl: string + issuerDidUrl: string + disclosureFrame?: DisclosureFrame + jsonWebAlgorithm?: JwaSignatureAlgorithm + hashingAlgorithm?: HashName +} + +export class SdJwtCredential = Record> { + public constructor(options: SdJwtCredentialOptions) { + this.payload = options.payload + this.holderDidUrl = options.holderDidUrl + this.issuerDidUrl = options.issuerDidUrl + this.disclosureFrame = options.disclosureFrame + this.jsonWebAlgorithm = options.jsonWebAlgorithm + this.hashingAlgorithm = options.hashingAlgorithm ?? 'sha2-256' + } + + public payload: Payload + public holderDidUrl: string + public issuerDidUrl: string + public disclosureFrame?: DisclosureFrame + public jsonWebAlgorithm?: JwaSignatureAlgorithm + public hashingAlgorithm?: HashName +} diff --git a/packages/sd-jwt-vc/src/SdJwtVcApi.ts b/packages/sd-jwt-vc/src/SdJwtVcApi.ts index cdd0eb49a0..24dbe00ea8 100644 --- a/packages/sd-jwt-vc/src/SdJwtVcApi.ts +++ b/packages/sd-jwt-vc/src/SdJwtVcApi.ts @@ -1,3 +1,4 @@ +import type { SdJwtCredential } from './SdJwtCredential' import type { SdJwtVcCreateOptions, SdJwtVcFromSerializedJwtOptions, @@ -32,6 +33,12 @@ export class SdJwtVcApi { return await this.sdJwtVcService.create(this.agentContext, payload, options) } + public async signCredential = Record>( + credential: SdJwtCredential + ): Promise<{ sdJwtVcRecord: SdJwtVcRecord; compact: string }> { + return await this.sdJwtVcService.signCredential(this.agentContext, credential) + } + /** * * Get and validate a sd-jwt-vc from a serialized JWT. diff --git a/packages/sd-jwt-vc/src/SdJwtVcService.ts b/packages/sd-jwt-vc/src/SdJwtVcService.ts index 561087c87d..e411a94c25 100644 --- a/packages/sd-jwt-vc/src/SdJwtVcService.ts +++ b/packages/sd-jwt-vc/src/SdJwtVcService.ts @@ -1,3 +1,4 @@ +import type { SdJwtCredential } from './SdJwtCredential' import type { SdJwtVcCreateOptions, SdJwtVcPresentOptions, @@ -98,6 +99,78 @@ export class SdJwtVcService { } } + public async signCredential = Record>( + agentContext: AgentContext, + sdJwtCredential: SdJwtCredential + ): Promise<{ sdJwtVcRecord: SdJwtVcRecord; compact: string }> { + const { holderDidUrl, issuerDidUrl, payload, disclosureFrame, hashingAlgorithm, jsonWebAlgorithm } = sdJwtCredential + + if (hashingAlgorithm !== 'sha2-256') { + throw new SdJwtVcError(`Unsupported hashing algorithm used: ${hashingAlgorithm}`) + } + + const parsedDid = parseDid(issuerDidUrl) + if (!parsedDid.fragment) { + throw new SdJwtVcError( + `issuer did url '${issuerDidUrl}' does not contain a '#'. Unable to derive key from did document` + ) + } + + const { verificationMethod: issuerVerificationMethod, didDocument: issuerDidDocument } = await this.resolveDidUrl( + agentContext, + issuerDidUrl + ) + const issuerKey = getKeyFromVerificationMethod(issuerVerificationMethod) + const alg = jsonWebAlgorithm ?? getJwkFromKey(issuerKey).supportedSignatureAlgorithms[0] + + const { verificationMethod: holderVerificationMethod } = await this.resolveDidUrl(agentContext, holderDidUrl) + const holderKey = getKeyFromVerificationMethod(holderVerificationMethod) + const holderKeyJwk = getJwkFromKey(holderKey).toJson() + + const header = { + alg: alg.toString(), + typ: 'vc+sd-jwt', + kid: parsedDid.fragment, + } + + const sdJwtVc = new SdJwtVc({}, { disclosureFrame }) + .withHasher(this.hasher) + .withSigner(this.signer(agentContext, issuerKey)) + .withSaltGenerator(agentContext.wallet.generateNonce) + .withHeader(header) + .withPayload({ ...payload }) + + // Add the `cnf` claim for the holder key binding + sdJwtVc.addPayloadClaim('cnf', { jwk: holderKeyJwk }) + + // Add the issuer DID as the `iss` claim + sdJwtVc.addPayloadClaim('iss', issuerDidDocument.id) + + // Add the issued at (iat) claim + sdJwtVc.addPayloadClaim('iat', Math.floor(new Date().getTime() / 1000)) + + const compact = await sdJwtVc.toCompact() + + if (!sdJwtVc.signature) { + throw new SdJwtVcError('Invalid sd-jwt-vc state. Signature should have been set when calling `toCompact`.') + } + + const sdJwtVcRecord = new SdJwtVcRecord({ + sdJwtVc: { + header: sdJwtVc.header, + payload: sdJwtVc.payload, + signature: sdJwtVc.signature, + disclosures: sdJwtVc.disclosures?.map((d) => d.decoded), + holderDidUrl, + }, + }) + + return { + sdJwtVcRecord, + compact, + } + } + public async create = Record>( agentContext: AgentContext, payload: Payload, diff --git a/packages/sd-jwt-vc/src/index.ts b/packages/sd-jwt-vc/src/index.ts index 18d611ca76..daae9ef404 100644 --- a/packages/sd-jwt-vc/src/index.ts +++ b/packages/sd-jwt-vc/src/index.ts @@ -2,4 +2,5 @@ export * from './SdJwtVcApi' export * from './SdJwtVcModule' export * from './SdJwtVcService' export * from './SdJwtVcError' +export * from './SdJwtCredential' export * from './repository' diff --git a/yarn.lock b/yarn.lock index 5ca24738fb..a0964ebccf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2569,7 +2569,7 @@ resolved "https://registry.yarnpkg.com/@sovpro/delimited-stream/-/delimited-stream-1.1.0.tgz#4334bba7ee241036e580fdd99c019377630d26b4" integrity sha512-kQpk267uxB19X3X2T1mvNMjyvIEonpNSHrMlK5ZaBU6aZxw7wPbpgKJOjHN3+/GPVpXgAV9soVT2oyHpLkLtyw== -"@sphereon/did-auth-siop@^0.5.0-unstable.7": +"@sphereon/did-auth-siop@^0.5.0-unstable.8": version "0.5.0-unstable.8" resolved "https://registry.yarnpkg.com/@sphereon/did-auth-siop/-/did-auth-siop-0.5.0-unstable.8.tgz#5bc9d66d6cedce5c6a39a54059e214c6046a83a9" integrity sha512-p7tuv9EaGv+U0lj8nBEZYotnUKrySh/rTYrGTH4c+gwJRZqiJCNoiLqfheC4lrMk0dnULd2WuuBe67qlxSaafQ== @@ -2602,7 +2602,7 @@ "@sphereon/oid4vci-client@file:../Sphereon/sphereon-oidvci-client-0.8.1": version "0.8.1" dependencies: - "@sphereon/oid4vci-common" "file:../../../Library/Caches/Yarn/v6/npm-@sphereon-oid4vci-client-0.8.1-ac00cd04-8774-4637-b885-eccc4730bff5-1701786850804/node_modules/@sphereon/sphereon-oid4vci-common-0.8.1" + "@sphereon/oid4vci-common" "file:../../../Library/Caches/Yarn/v6/npm-@sphereon-oid4vci-client-0.8.1-9e507f08-cbb1-4d9a-b896-929b2cd56c8c-1702036843167/node_modules/@sphereon/sphereon-oid4vci-common-0.8.1" "@sphereon/ssi-types" "0.17.2" cross-fetch "^3.1.8" debug "^4.3.4" @@ -2617,8 +2617,8 @@ "@sphereon/oid4vci-issuer-server@file:../Sphereon/sphereon-oid4vci-issuer-server-0.8.1": version "0.8.1" dependencies: - "@sphereon/oid4vci-common" "file:../../../Library/Caches/Yarn/v6/npm-@sphereon-oid4vci-issuer-server-0.8.1-f2353e87-1560-4059-8ec7-6727944a622f-1701786850800/node_modules/@sphereon/sphereon-oid4vci-common-0.8.1" - "@sphereon/oid4vci-issuer" "file:../../../Library/Caches/Yarn/v6/npm-@sphereon-oid4vci-issuer-server-0.8.1-f2353e87-1560-4059-8ec7-6727944a622f-1701786850800/node_modules/@sphereon/sphereon-oid4vci-issuer-0.8.1" + "@sphereon/oid4vci-common" "file:../../../Library/Caches/Yarn/v6/npm-@sphereon-oid4vci-issuer-server-0.8.1-59950255-e85d-4321-ab13-55be7622189a-1702036843161/node_modules/@sphereon/sphereon-oid4vci-common-0.8.1" + "@sphereon/oid4vci-issuer" "file:../../../Library/Caches/Yarn/v6/npm-@sphereon-oid4vci-issuer-server-0.8.1-59950255-e85d-4321-ab13-55be7622189a-1702036843161/node_modules/@sphereon/sphereon-oid4vci-issuer-0.8.1" "@sphereon/ssi-express-support" "0.17.2" "@sphereon/ssi-types" "0.17.2" body-parser "^1.20.2" From eab4da8ec2642c924a7800d9a891890c3516ad87 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Mon, 11 Dec 2023 10:43:15 +0100 Subject: [PATCH 082/115] fix: remove todos --- .../presentation/PresentationExchangeService.ts | 1 - packages/openid4vc/tests/utilsVp.ts | 8 +------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/openid4vc/src/openid4vc-holder/presentation/PresentationExchangeService.ts b/packages/openid4vc/src/openid4vc-holder/presentation/PresentationExchangeService.ts index 942c099577..af98169252 100644 --- a/packages/openid4vc/src/openid4vc-holder/presentation/PresentationExchangeService.ts +++ b/packages/openid4vc/src/openid4vc-holder/presentation/PresentationExchangeService.ts @@ -92,7 +92,6 @@ export class PresentationExchangeService { // The schema.uri can contain either an expanded type, or a context uri for (const inputDescriptor of pd.input_descriptors) { for (const schema of inputDescriptor.schema) { - // TODO: write migration query.push({ $or: [{ expandedType: [schema.uri] }, { contexts: [schema.uri] }, { type: [schema.uri] }], }) diff --git a/packages/openid4vc/tests/utilsVp.ts b/packages/openid4vc/tests/utilsVp.ts index 768e469c3e..4baef34bdd 100644 --- a/packages/openid4vc/tests/utilsVp.ts +++ b/packages/openid4vc/tests/utilsVp.ts @@ -5,13 +5,7 @@ import { SigningAlgo } from '@sphereon/did-auth-siop' import { staticOpOpenIdConfig } from '../src' import { staticOpSiopConfig } from '../src/openid4vc-verifier/OpenId4VcVerifierServiceOptions' -// id id%22%3A%22test%22%2C%22 -// * = %2A -// TODO: error on sphereon lib PR opened -// TODO: walt issued credentials verification fails due to some time issue || //throw new Error(`Inconsistent issuance dates between JWT claim (${nbfDateAsStr}) and VC value (${issuanceDate})`); -// TODO: error walt no id in presentation definition -// TODO: error walt vc.type is an array not a string thus the filter does not work $.type (should be array according to vc data 1.1) -// TODO: jwt_vc vs jwt_vc_json + export const waltPortalOpenBadgeJwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa3RpUVFFcW0yeWFwWEJEdDFXRVZCM2RxZ3Z5emk5NkZ1RkFOWW1yZ1RyS1Y5I3o2TWt0aVFRRXFtMnlhcFhCRHQxV0VWQjNkcWd2eXppOTZGdUZBTlltcmdUcktWOSJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiT3BlbkJhZGdlQ3JlZGVudGlhbCJdLCJjcmVkZW50aWFsU3ViamVjdCI6e319LCJpc3MiOiJkaWQ6a2V5Ono2TWt0aVFRRXFtMnlhcFhCRHQxV0VWQjNkcWd2eXppOTZGdUZBTlltcmdUcktWOSIsInN1YiI6ImRpZDprZXk6ejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjIiwibmJmIjoxNzAwNzQzMzM1fQ.OcKPyaWeVV-78BWr8N4h2Cyvjtc9jzknAqvTA77hTbKCNCEbhGboo-S6yXHLC-3NWYQ1vVcqZmdPlIOrHZ7MDw' From 4e36a999d9ff53e84f71767dac98e67ae4634d20 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Thu, 14 Dec 2023 13:52:58 +0100 Subject: [PATCH 083/115] feat: ldp_vc proof requests --- .../__tests__/openid4vp-holder.e2e.test.ts | 122 ++++++++++++++++++ .../PresentationExchangeService.ts | 33 ++--- .../selection/PexCredentialSelection.ts | 15 +-- packages/openid4vc/tests/utilsVp.ts | 56 ++++++++ 4 files changed, 198 insertions(+), 28 deletions(-) diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts index b2fc4793ce..5a986dc550 100644 --- a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts @@ -12,7 +12,9 @@ import nock from 'nock' import { OpenId4VcHolderModule } from '..' import { createAgentFromModules } from '../../../tests/utils' import { + openBadgeCredentialPresentationDefinitionLdpVc, combinePresentationDefinitions, + getOpenBadgeCredentialLdpVc, openBadgePresentationDefinition, staticOpOpenIdConfigEdDSA, universityDegreePresentationDefinition, @@ -482,6 +484,126 @@ describe('OpenId4VcHolder | OpenID4VP', () => { }) }) + it('expect vp request with single requested ldp_vc credential to succeed', async () => { + const credential = await getOpenBadgeCredentialLdpVc( + verifier.agent.context, + verifier.verificationMethod, + holder.verificationMethod + ) + await holder.agent.w3cCredentials.storeCredential({ + credential, + }) + + const createProofRequestOptions: CreateProofRequestOptions = { + verificationMethod: verifier.verificationMethod, + holderMetadata: staticOpOpenIdConfigEdDSA, + presentationDefinition: openBadgeCredentialPresentationDefinitionLdpVc, + } + + const { proofRequest, proofRequestMetadata } = await verifier.agent.modules.openId4VcVerifier.createProofRequest( + createProofRequestOptions + ) + + //////////////////////////// OP (validate and parse the request) //////////////////////////// + const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(proofRequest) + if (result.proofType === 'authentication') throw new Error('Expected a proofRequest') + + //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// + // Select the appropriate credentials + + if (!result.presentationSubmission.areRequirementsSatisfied) { + throw new Error('Requirements are not satisfied.') + } + + //////////////////////////// OP (accept the verified request) //////////////////////////// + const { submittedResponse, status } = await holder.agent.modules.openId4VcHolder.acceptPresentationRequest( + result.presentationRequest, + { + submission: result.presentationSubmission, + submissionEntryIndexes: [0], + } + ) + + expect(status).toBe(200) + + // The RP MUST validate that the aud (audience) Claim contains the value of the client_id + // that the RP sent in the Authorization Request as an audience. + // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. + const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyProofResponse( + submittedResponse + ) + + const { state, challenge } = proofRequestMetadata + expect(idTokenPayload).toBeDefined() + expect(idTokenPayload.state).toMatch(state) + expect(idTokenPayload.nonce).toMatch(challenge) + + expect(submission).toBeDefined() + expect(submission?.presentationDefinitions).toHaveLength(1) + expect(submission?.submissionData.definition_id).toBe('OpenBadgeCredential') + expect(submission?.presentations).toHaveLength(1) + expect(submission?.presentations[0].vcs[0].credential.type).toEqual(['VerifiableCredential', 'OpenBadgeCredential']) + + await waitForMockFunction(mockFunction) + expect(mockFunction).toBeCalledWith({ + idTokenPayload: expect.objectContaining(idTokenPayload), + submission: expect.objectContaining(submission), + }) + }) + + it('expects the submission to fail if there are too few submission entry indexes, and also to fail when requesting two different presentation formats', async () => { + const credential = await getOpenBadgeCredentialLdpVc( + verifier.agent.context, + verifier.verificationMethod, + holder.verificationMethod + ) + + await holder.agent.w3cCredentials.storeCredential({ credential }) + + await holder.agent.w3cCredentials.storeCredential({ + credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), + }) + + const createProofRequestOptions: CreateProofRequestOptions = { + verificationMethod: verifier.verificationMethod, + holderMetadata: staticOpOpenIdConfigEdDSA, + presentationDefinition: combinePresentationDefinitions([ + universityDegreePresentationDefinition, + openBadgeCredentialPresentationDefinitionLdpVc, + ]), + } + + const { proofRequest } = await verifier.agent.modules.openId4VcVerifier.createProofRequest( + createProofRequestOptions + ) + + //////////////////////////// OP (validate and parse the request) //////////////////////////// + const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(proofRequest) + if (result.proofType === 'authentication') throw new Error('Expected a proofRequest') + + //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// + // Select the appropriate credentials + + if (!result.presentationSubmission.areRequirementsSatisfied) { + throw new Error('Requirements are not satisfied.') + } + + //////////////////////////// OP (accept the verified request) //////////////////////////// + await expect( + holder.agent.modules.openId4VcHolder.acceptPresentationRequest(result.presentationRequest, { + submission: result.presentationSubmission, + submissionEntryIndexes: [0, 0], + }) + ).rejects.toThrow() + + await expect( + holder.agent.modules.openId4VcHolder.acceptPresentationRequest(result.presentationRequest, { + submission: result.presentationSubmission, + submissionEntryIndexes: [0], + }) + ).rejects.toThrow() + }) + // it('edited walt vp request', async () => { // const credential = W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt) // await holder.w3cCredentials.storeCredential({ credential }) diff --git a/packages/openid4vc/src/openid4vc-holder/presentation/PresentationExchangeService.ts b/packages/openid4vc/src/openid4vc-holder/presentation/PresentationExchangeService.ts index af98169252..a60d376895 100644 --- a/packages/openid4vc/src/openid4vc-holder/presentation/PresentationExchangeService.ts +++ b/packages/openid4vc/src/openid4vc-holder/presentation/PresentationExchangeService.ts @@ -109,10 +109,7 @@ export class PresentationExchangeService { // query the wallet ourselves first to avoid the need to query the pex library for all // credentials for every proof request - const credentialRecords = await w3cCredentialRepository.findByQuery(agentContext, { - $or: query, - }) - + const credentialRecords = await w3cCredentialRepository.findByQuery(agentContext, { $or: query }) return credentialRecords } @@ -387,19 +384,15 @@ export class PresentationExchangeService { ) } - if (suitableSignatureSuites) { - if (suitableSignatureSuites.includes(supportedSignatureSuite.proofType) === false) { - throw new AriesFrameworkError( - [ - 'No possible signature suite found for the given verification method.', - `Verification method type: ${verificationMethod.type}`, - `SupportedSignatureSuite '${supportedSignatureSuite.proofType}'`, - `SuitableSignatureSuites: ${suitableSignatureSuites.join(', ')}`, - ].join('\n') - ) - } - - return supportedSignatureSuite.proofType + if (suitableSignatureSuites && suitableSignatureSuites.includes(supportedSignatureSuite.proofType) === false) { + throw new AriesFrameworkError( + [ + 'No possible signature suite found for the given verification method.', + `Verification method type: ${verificationMethod.type}`, + `SupportedSignatureSuite '${supportedSignatureSuite.proofType}'`, + `SuitableSignatureSuites: ${suitableSignatureSuites.join(', ')}`, + ].join('\n') + ) } return supportedSignatureSuite.proofType @@ -425,7 +418,7 @@ export class PresentationExchangeService { } // Clients MUST ignore any presentation_submission element included inside a Verifiable Presentation. - const presentationToSign = { ...presentationJson, presentation_submission: undefined } + delete presentationJson.presentation_submission let signedPresentation: W3cVerifiablePresentation if (vpFormat === 'jwt_vp') { @@ -433,7 +426,7 @@ export class PresentationExchangeService { format: ClaimFormat.JwtVp, alg: this.getSigningAlgorithmForJwtVc(presentationDefinition, verificationMethod), verificationMethod: verificationMethod.id, - presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation), + presentation: JsonTransformer.fromJSON(presentationJson, W3cPresentation), challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), domain, }) @@ -443,7 +436,7 @@ export class PresentationExchangeService { proofType: this.getProofTypeForLdpVc(agentContext, presentationDefinition, verificationMethod), proofPurpose: 'authentication', verificationMethod: verificationMethod.id, - presentation: JsonTransformer.fromJSON(presentationToSign, W3cPresentation), + presentation: JsonTransformer.fromJSON(presentationJson, W3cPresentation), challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), domain, }) diff --git a/packages/openid4vc/src/openid4vc-holder/presentation/selection/PexCredentialSelection.ts b/packages/openid4vc/src/openid4vc-holder/presentation/selection/PexCredentialSelection.ts index b3fb3ad5f9..f51bcbf3bf 100644 --- a/packages/openid4vc/src/openid4vc-holder/presentation/selection/PexCredentialSelection.ts +++ b/packages/openid4vc/src/openid4vc-holder/presentation/selection/PexCredentialSelection.ts @@ -3,7 +3,7 @@ import type { W3cCredentialRecord } from '@aries-framework/core' import type { IPresentationDefinition, SelectResults, SubmissionRequirementMatch } from '@sphereon/pex' import type { InputDescriptorV1, InputDescriptorV2, SubmissionRequirement } from '@sphereon/pex-models' -import { AriesFrameworkError } from '@aries-framework/core' +import { AriesFrameworkError, deepEquality } from '@aries-framework/core' import { PEX } from '@sphereon/pex' import { Rules } from '@sphereon/pex-models' import { default as jp } from 'jsonpath' @@ -15,16 +15,12 @@ export async function selectCredentialsForRequest( credentialRecords: W3cCredentialRecord[], holderDIDs: string[] ): Promise { - const encodedCredentials = credentialRecords.map((c) => getSphereonOriginalVerifiableCredential(c.credential)) - - if (!presentationDefinition) { - throw new AriesFrameworkError('Presentation Definition is required to select credentials for submission.') - } + const sphereonEncodedCredentials = credentialRecords.map((c) => getSphereonOriginalVerifiableCredential(c.credential)) const pex = new PEX() // FIXME: there is a function for this in the VP library, but it is not usable atm - const selectResultsRaw = pex.selectFrom(presentationDefinition, encodedCredentials, { + const selectResultsRaw = pex.selectFrom(presentationDefinition, sphereonEncodedCredentials, { holderDIDs, // limitDisclosureSignatureSuites: [], // restrictToDIDMethods, @@ -35,7 +31,10 @@ export async function selectCredentialsForRequest( ...selectResultsRaw, // Map the encoded credential to their respective w3c credential record verifiableCredential: selectResultsRaw.verifiableCredential?.map((encoded) => { - const credentialIndex = encodedCredentials.indexOf(encoded) + const credentialIndex = + typeof encoded === 'string' + ? sphereonEncodedCredentials.indexOf(encoded) + : sphereonEncodedCredentials.findIndex((sphereonEncoded) => deepEquality(encoded, sphereonEncoded)) const credentialRecord = credentialRecords[credentialIndex] if (!credentialRecord) throw new AriesFrameworkError('Unable to find credential in credential records.') diff --git a/packages/openid4vc/tests/utilsVp.ts b/packages/openid4vc/tests/utilsVp.ts index 4baef34bdd..8e741272c9 100644 --- a/packages/openid4vc/tests/utilsVp.ts +++ b/packages/openid4vc/tests/utilsVp.ts @@ -1,10 +1,20 @@ import type { HolderMetadata } from '../src' +import type { AgentContext, VerificationMethod } from '@aries-framework/core' import type { PresentationDefinitionV2 } from '@sphereon/pex-models' +import { + W3cCredential, + W3cIssuer, + W3cCredentialSubject, + W3cCredentialService, + ClaimFormat, + CREDENTIALS_CONTEXT_V1_URL, +} from '@aries-framework/core' import { SigningAlgo } from '@sphereon/did-auth-siop' import { staticOpOpenIdConfig } from '../src' import { staticOpSiopConfig } from '../src/openid4vc-verifier/OpenId4VcVerifierServiceOptions' +import { getProofTypeFromVerificationMethod } from '../src/shared/utils' export const waltPortalOpenBadgeJwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa3RpUVFFcW0yeWFwWEJEdDFXRVZCM2RxZ3Z5emk5NkZ1RkFOWW1yZ1RyS1Y5I3o2TWt0aVFRRXFtMnlhcFhCRHQxV0VWQjNkcWd2eXppOTZGdUZBTlltcmdUcktWOSJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiT3BlbkJhZGdlQ3JlZGVudGlhbCJdLCJjcmVkZW50aWFsU3ViamVjdCI6e319LCJpc3MiOiJkaWQ6a2V5Ono2TWt0aVFRRXFtMnlhcFhCRHQxV0VWQjNkcWd2eXppOTZGdUZBTlltcmdUcktWOSIsInN1YiI6ImRpZDprZXk6ejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjIiwibmJmIjoxNzAwNzQzMzM1fQ.OcKPyaWeVV-78BWr8N4h2Cyvjtc9jzknAqvTA77hTbKCNCEbhGboo-S6yXHLC-3NWYQ1vVcqZmdPlIOrHZ7MDw' @@ -12,6 +22,51 @@ export const waltPortalOpenBadgeJwt = export const waltUniversityDegreeJwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa3RpUVFFcW0yeWFwWEJEdDFXRVZCM2RxZ3Z5emk5NkZ1RkFOWW1yZ1RyS1Y5I3o2TWt0aVFRRXFtMnlhcFhCRHQxV0VWQjNkcWd2eXppOTZGdUZBTlltcmdUcktWOSJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVW5pdmVyc2l0eURlZ3JlZUNyZWRlbnRpYWwiXSwiY3JlZGVudGlhbFN1YmplY3QiOnt9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdGlRUUVxbTJ5YXBYQkR0MVdFVkIzZHFndnl6aTk2RnVGQU5ZbXJnVHJLVjkiLCJzdWIiOiJkaWQ6a2V5Ono2TWtwR1I0Z3M0UmMzWnBoNHZqOHdSbmpuQXhnQVBTeGNSOE1BVkt1dFdzcFF6YyIsIm5iZiI6MTcwMDc0MzM5NH0.EhMnE349oOvzbu0rFl-m_7FOoRsB5VucLV5tUUIW0jPxkJ7J0qVLOJTXVX4KNv_N9oeP8pgTUvydd6nxB_0KCQ' +export const getOpenBadgeCredentialLdpVc = async ( + agentContext: AgentContext, + issuerVerificationMethod: VerificationMethod, + holderVerificationMethod: VerificationMethod +) => { + const credential = new W3cCredential({ + context: [CREDENTIALS_CONTEXT_V1_URL, 'https://www.w3.org/2018/credentials/examples/v1'], + type: ['VerifiableCredential', 'OpenBadgeCredential'], + id: 'http://example.edu/credentials/3732', + issuer: new W3cIssuer({ + id: issuerVerificationMethod.controller, + }), + issuanceDate: '2017-10-22T12:23:48Z', + expirationDate: '2027-10-22T12:23:48Z', + credentialSubject: new W3cCredentialSubject({ + id: holderVerificationMethod.controller, + }), + }) + + const w3cs = agentContext.dependencyManager.resolve(W3cCredentialService) + const proofType = getProofTypeFromVerificationMethod(agentContext, holderVerificationMethod) + const signedLdpVc = await w3cs.signCredential(agentContext, { + format: ClaimFormat.LdpVc, + credential, + verificationMethod: issuerVerificationMethod.id, + proofType, + }) + + return signedLdpVc +} +export const openBadgeCredentialPresentationDefinitionLdpVc: PresentationDefinitionV2 = { + id: 'OpenBadgeCredential', + input_descriptors: [ + { + id: 'OpenBadgeCredential', + // changed jwt_vc_json to jwt_vc + format: { ldp_vc: { proof_type: ['Ed25519Signature2018'] } }, + // changed $.type to $.vc.type + constraints: { + fields: [{ path: ['$.type.*', '$.vc.type'], filter: { type: 'string', pattern: 'OpenBadgeCredential' } }], + }, + }, + ], +} + export const universityDegreePresentationDefinition: PresentationDefinitionV2 = { id: 'UniversityDegreeCredential', input_descriptors: [ @@ -65,6 +120,7 @@ export const staticSiopConfigEDDSA: HolderMetadata = { vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] }, jwt_vp: { alg: [SigningAlgo.EDDSA] } }, } +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function waitForMockFunction(mockFn: jest.Mock) { return new Promise((resolve, reject) => { const intervalId = setInterval(() => { From 3d9b8423b4553b9c5d72f408e90f57e9dd282037 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Fri, 15 Dec 2023 14:17:21 +0100 Subject: [PATCH 084/115] fix: endpoints should not be absolute --- demo-openid/src/Issuer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demo-openid/src/Issuer.ts b/demo-openid/src/Issuer.ts index 064ca96b2b..a6f281f0d7 100644 --- a/demo-openid/src/Issuer.ts +++ b/demo-openid/src/Issuer.ts @@ -49,8 +49,8 @@ function getOpenIdIssuerModules() { openId4VcIssuer: new OpenId4VcIssuerModule({ issuerMetadata: { issuerBaseUrl: 'http://localhost:2000', - tokenEndpointPath: 'http://localhost:2000/token', - credentialEndpointPath: 'http://localhost:2000/credentials', + tokenEndpointPath: '/token', + credentialEndpointPath: '/credentials', credentialsSupported, }, }), From c7d9ba78e10f06d4beb1c325510e90925fb015f7 Mon Sep 17 00:00:00 2001 From: Martin Auer Date: Thu, 4 Jan 2024 14:18:23 +0100 Subject: [PATCH 085/115] fix: migration --- .../vc/repository/__tests__/W3cCredentialRecord.test.ts | 1 + packages/core/src/storage/migration/updates.ts | 6 ++++++ .../updates/0.4-0.5/__tests__/w3cCredentialRecord.test.ts | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts b/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts index e64caa1f95..9371f36feb 100644 --- a/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts +++ b/packages/core/src/modules/vc/repository/__tests__/W3cCredentialRecord.test.ts @@ -27,6 +27,7 @@ describe('W3cCredentialRecord', () => { proofTypes: credential.proofTypes, givenId: credential.id, expandedTypes: ['https://expanded.tag#1'], + types: ['VerifiableCredential', 'UniversityDegreeCredential'], }) }) }) diff --git a/packages/core/src/storage/migration/updates.ts b/packages/core/src/storage/migration/updates.ts index 4e1d09a898..9ee3a080e0 100644 --- a/packages/core/src/storage/migration/updates.ts +++ b/packages/core/src/storage/migration/updates.ts @@ -6,6 +6,7 @@ import { updateV0_1ToV0_2 } from './updates/0.1-0.2' import { updateV0_2ToV0_3 } from './updates/0.2-0.3' import { updateV0_3ToV0_3_1 } from './updates/0.3-0.3.1' import { updateV0_3_1ToV0_4 } from './updates/0.3.1-0.4' +import { updateV0_4ToV0_5 } from './updates/0.4-0.5' export const INITIAL_STORAGE_VERSION = '0.1' @@ -46,6 +47,11 @@ export const supportedUpdates = [ toVersion: '0.4', doUpdate: updateV0_3_1ToV0_4, }, + { + fromVersion: '0.3.1', + toVersion: '0.4', + doUpdate: updateV0_4ToV0_5, + }, ] as const // Current version is last toVersion from the supported updates diff --git a/packages/core/src/storage/migration/updates/0.4-0.5/__tests__/w3cCredentialRecord.test.ts b/packages/core/src/storage/migration/updates/0.4-0.5/__tests__/w3cCredentialRecord.test.ts index 31fa8631c4..d366426d82 100644 --- a/packages/core/src/storage/migration/updates/0.4-0.5/__tests__/w3cCredentialRecord.test.ts +++ b/packages/core/src/storage/migration/updates/0.4-0.5/__tests__/w3cCredentialRecord.test.ts @@ -57,7 +57,7 @@ describe('0.4-0.5 | W3cCredentialRecord', () => { expect(repository.update).toHaveBeenCalledTimes(1) const [, record] = mockFunction(repository.update).mock.calls[0] - expect(record.getTags().claimFormat).toEqual('ldp_vc') + expect(record.getTags().types).toEqual(['VerifiableCredential', 'UniversityDegreeCredential']) }) }) }) From 4228f77221fdda418c9dba59ffa205fe46b10666 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 8 Jan 2024 13:45:39 +0700 Subject: [PATCH 086/115] feat(tenants): expose additional record methods Signed-off-by: Timo Glastra --- packages/tenants/src/TenantsApi.ts | 23 +++++++++++++------ .../src/services/TenantRecordService.ts | 10 +++++++- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/tenants/src/TenantsApi.ts b/packages/tenants/src/TenantsApi.ts index e29b487b14..b16d02e17b 100644 --- a/packages/tenants/src/TenantsApi.ts +++ b/packages/tenants/src/TenantsApi.ts @@ -1,5 +1,6 @@ import type { CreateTenantOptions, GetTenantAgentOptions, WithTenantAgentCallback } from './TenantsApiOptions' -import type { DefaultAgentModules, ModulesMap } from '@aries-framework/core' +import type { TenantRecord } from './repository' +import type { DefaultAgentModules, ModulesMap, Query } from '@aries-framework/core' import { AgentContext, inject, InjectionSymbols, AgentContextProvider, injectable, Logger } from '@aries-framework/core' @@ -8,19 +9,19 @@ import { TenantRecordService } from './services' @injectable() export class TenantsApi { - private agentContext: AgentContext + public readonly rootAgentContext: AgentContext private tenantRecordService: TenantRecordService private agentContextProvider: AgentContextProvider private logger: Logger public constructor( tenantRecordService: TenantRecordService, - agentContext: AgentContext, + rootAgentContext: AgentContext, @inject(InjectionSymbols.AgentContextProvider) agentContextProvider: AgentContextProvider, @inject(InjectionSymbols.Logger) logger: Logger ) { this.tenantRecordService = tenantRecordService - this.agentContext = agentContext + this.rootAgentContext = rootAgentContext this.agentContextProvider = agentContextProvider this.logger = logger } @@ -58,7 +59,7 @@ export class TenantsApi { public async createTenant(options: CreateTenantOptions) { this.logger.debug(`Creating tenant with label ${options.config.label}`) - const tenantRecord = await this.tenantRecordService.createTenant(this.agentContext, options.config) + const tenantRecord = await this.tenantRecordService.createTenant(this.rootAgentContext, options.config) // This initializes the tenant agent, creates the wallet etc... const tenantAgent = await this.getTenantAgent({ tenantId: tenantRecord.id }) @@ -71,7 +72,7 @@ export class TenantsApi { public async getTenantById(tenantId: string) { this.logger.debug(`Getting tenant by id '${tenantId}'`) - return this.tenantRecordService.getTenantById(this.agentContext, tenantId) + return this.tenantRecordService.getTenantById(this.rootAgentContext, tenantId) } public async deleteTenantById(tenantId: string) { @@ -84,6 +85,14 @@ export class TenantsApi { this.logger.trace(`Shutting down agent for tenant '${tenantId}'`) await tenantAgent.endSession() - return this.tenantRecordService.deleteTenantById(this.agentContext, tenantId) + return this.tenantRecordService.deleteTenantById(this.rootAgentContext, tenantId) + } + + public async updateTenant(tenant: TenantRecord) { + await this.tenantRecordService.updateTenant(this.rootAgentContext, tenant) + } + + public async findTenantsByQuery(query: Query) { + return this.tenantRecordService.findTenantsByQuery(this.rootAgentContext, query) } } diff --git a/packages/tenants/src/services/TenantRecordService.ts b/packages/tenants/src/services/TenantRecordService.ts index 3b690d7c3c..460add6e98 100644 --- a/packages/tenants/src/services/TenantRecordService.ts +++ b/packages/tenants/src/services/TenantRecordService.ts @@ -1,5 +1,5 @@ import type { TenantConfig } from '../models/TenantConfig' -import type { AgentContext, Key } from '@aries-framework/core' +import type { AgentContext, Key, Query } from '@aries-framework/core' import { injectable, utils, KeyDerivationMethod } from '@aries-framework/core' @@ -60,6 +60,14 @@ export class TenantRecordService { await this.tenantRepository.delete(agentContext, tenantRecord) } + public async updateTenant(agentContext: AgentContext, tenantRecord: TenantRecord) { + return this.tenantRepository.update(agentContext, tenantRecord) + } + + public async findTenantsByQuery(agentContext: AgentContext, query: Query) { + return this.tenantRepository.findByQuery(agentContext, query) + } + public async findTenantRoutingRecordByRecipientKey( agentContext: AgentContext, recipientKey: Key From fd355cd6adf26023e01b1ab1b5aca6d46f296011 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 8 Jan 2024 13:46:35 +0700 Subject: [PATCH 087/115] feat: add joinUriParts method Signed-off-by: Timo Glastra --- packages/core/src/index.ts | 2 +- packages/core/src/utils/path.ts | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ee640ce26c..1bb7010de2 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -38,7 +38,7 @@ export { Repository } from './storage/Repository' export * from './storage/RepositoryEvents' export { StorageService, Query, SimpleQuery, BaseRecordConstructor } from './storage/StorageService' export * from './storage/migration' -export { getDirFromFilePath } from './utils/path' +export { getDirFromFilePath, joinUriParts } from './utils/path' export { InjectionSymbols } from './constants' export * from './wallet' export type { TransportSession } from './agent/TransportService' diff --git a/packages/core/src/utils/path.ts b/packages/core/src/utils/path.ts index 8b4dc2c26b..0f9196cdc1 100644 --- a/packages/core/src/utils/path.ts +++ b/packages/core/src/utils/path.ts @@ -7,3 +7,19 @@ export function getDirFromFilePath(path: string) { return path.substring(0, Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))) } + +/** + * Combine multiple uri parts into a single uri taking into account slashes. + * + * @param parts the parts to combine + * @returns the combined url + */ +export function joinUriParts(parts: string[]) { + let combined = '' + + for (const part of parts) { + combined += part.endsWith('/') ? part : `${part}/` + } + + return combined +} From 2c94c610f90a645ded685e54c8a67b3205fdfd27 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 8 Jan 2024 14:18:33 +0700 Subject: [PATCH 088/115] feat(sd-jwt): update to latest version and improvementts Signed-off-by: Timo Glastra --- packages/sd-jwt-vc/package.json | 2 +- packages/sd-jwt-vc/src/SdJwtCredential.ts | 7 +- packages/sd-jwt-vc/src/SdJwtVcApi.ts | 35 +++++---- packages/sd-jwt-vc/src/SdJwtVcModule.ts | 3 - packages/sd-jwt-vc/src/SdJwtVcOptions.ts | 22 +++--- packages/sd-jwt-vc/src/SdJwtVcService.ts | 73 +++++++++---------- .../src/__tests__/SdJwtVcService.test.ts | 69 +++++++++++++----- .../src/__tests__/sdjwtvc.fixtures.ts | 12 +-- .../sd-jwt-vc/src/repository/SdJwtVcRecord.ts | 25 +++---- .../__tests__/SdJwtVcRecord.test.ts | 13 ++-- packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts | 29 ++++++-- 11 files changed, 170 insertions(+), 120 deletions(-) diff --git a/packages/sd-jwt-vc/package.json b/packages/sd-jwt-vc/package.json index 62f2925c63..d6ac35e2b6 100644 --- a/packages/sd-jwt-vc/package.json +++ b/packages/sd-jwt-vc/package.json @@ -28,7 +28,7 @@ "@aries-framework/core": "^0.4.2", "class-transformer": "0.5.1", "class-validator": "0.14.0", - "jwt-sd": "^0.1.2" + "@sd-jwt/core": "0.1.2-alpha.0" }, "devDependencies": { "@aries-framework/node": "^0.4.2", diff --git a/packages/sd-jwt-vc/src/SdJwtCredential.ts b/packages/sd-jwt-vc/src/SdJwtCredential.ts index ac43aff8b8..b518089b0a 100644 --- a/packages/sd-jwt-vc/src/SdJwtCredential.ts +++ b/packages/sd-jwt-vc/src/SdJwtCredential.ts @@ -1,7 +1,8 @@ +import type { SdJwtVcPayload } from './SdJwtVcOptions' import type { HashName, JwaSignatureAlgorithm } from '@aries-framework/core' -import type { DisclosureFrame } from 'jwt-sd' +import type { DisclosureFrame } from '@sd-jwt/core' -export interface SdJwtCredentialOptions = Record> { +export interface SdJwtCredentialOptions { payload: Payload holderDidUrl: string issuerDidUrl: string @@ -10,7 +11,7 @@ export interface SdJwtCredentialOptions hashingAlgorithm?: HashName } -export class SdJwtCredential = Record> { +export class SdJwtCredential { public constructor(options: SdJwtCredentialOptions) { this.payload = options.payload this.holderDidUrl = options.holderDidUrl diff --git a/packages/sd-jwt-vc/src/SdJwtVcApi.ts b/packages/sd-jwt-vc/src/SdJwtVcApi.ts index 24dbe00ea8..0fed115410 100644 --- a/packages/sd-jwt-vc/src/SdJwtVcApi.ts +++ b/packages/sd-jwt-vc/src/SdJwtVcApi.ts @@ -2,10 +2,11 @@ import type { SdJwtCredential } from './SdJwtCredential' import type { SdJwtVcCreateOptions, SdJwtVcFromSerializedJwtOptions, + SdJwtVcHeader, + SdJwtVcPayload, SdJwtVcPresentOptions, SdJwtVcVerifyOptions, } from './SdJwtVcOptions' -import type { SdJwtVcVerificationResult } from './SdJwtVcService' import type { SdJwtVcRecord } from './repository' import type { Query } from '@aries-framework/core' @@ -26,16 +27,11 @@ export class SdJwtVcApi { this.sdJwtVcService = sdJwtVcService } - public async create = Record>( - payload: Payload, - options: SdJwtVcCreateOptions - ): Promise<{ sdJwtVcRecord: SdJwtVcRecord; compact: string }> { + public async create(payload: Payload, options: SdJwtVcCreateOptions) { return await this.sdJwtVcService.create(this.agentContext, payload, options) } - public async signCredential = Record>( - credential: SdJwtCredential - ): Promise<{ sdJwtVcRecord: SdJwtVcRecord; compact: string }> { + public async signCredential(credential: SdJwtCredential) { return await this.sdJwtVcService.signCredential(this.agentContext, credential) } @@ -43,8 +39,11 @@ export class SdJwtVcApi { * * Get and validate a sd-jwt-vc from a serialized JWT. */ - public async fromSerializedJwt(sdJwtVcCompact: string, options: SdJwtVcFromSerializedJwtOptions) { - return await this.sdJwtVcService.fromSerializedJwt(this.agentContext, sdJwtVcCompact, options) + public async fromSerializedJwt
( + sdJwtVcCompact: string, + options: SdJwtVcFromSerializedJwtOptions + ) { + return await this.sdJwtVcService.fromSerializedJwt(this.agentContext, sdJwtVcCompact, options) } /** @@ -52,7 +51,7 @@ export class SdJwtVcApi { * Stores and sd-jwt-vc record * */ - public async storeCredential(sdJwtVcRecord: SdJwtVcRecord): Promise { + public async storeCredential(sdJwtVcRecord: SdJwtVcRecord) { return await this.sdJwtVcService.storeCredential(this.agentContext, sdJwtVcRecord) } @@ -65,8 +64,11 @@ export class SdJwtVcApi { * Also, whether to include the holder key binding. * */ - public async present(sdJwtVcRecord: SdJwtVcRecord, options: SdJwtVcPresentOptions): Promise { - return await this.sdJwtVcService.present(this.agentContext, sdJwtVcRecord, options) + public async present
( + sdJwtVcRecord: SdJwtVcRecord, + options: SdJwtVcPresentOptions + ): Promise { + return await this.sdJwtVcService.present(this.agentContext, sdJwtVcRecord, options) } /** @@ -76,13 +78,10 @@ export class SdJwtVcApi { * For example, you might still want to continue with a flow if not all the claims are included, but the signature is valid. * */ - public async verify< - Header extends Record = Record, - Payload extends Record = Record - >( + public async verify
( sdJwtVcCompact: string, options: SdJwtVcVerifyOptions - ): Promise<{ sdJwtVcRecord: SdJwtVcRecord; validation: SdJwtVcVerificationResult }> { + ) { return await this.sdJwtVcService.verify(this.agentContext, sdJwtVcCompact, options) } diff --git a/packages/sd-jwt-vc/src/SdJwtVcModule.ts b/packages/sd-jwt-vc/src/SdJwtVcModule.ts index eea361477f..affe7fd01d 100644 --- a/packages/sd-jwt-vc/src/SdJwtVcModule.ts +++ b/packages/sd-jwt-vc/src/SdJwtVcModule.ts @@ -23,9 +23,6 @@ export class SdJwtVcModule implements Module { "The '@aries-framework/sd-jwt-vc' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." ) - // Api - dependencyManager.registerContextScoped(this.api) - // Services dependencyManager.registerSingleton(SdJwtVcService) diff --git a/packages/sd-jwt-vc/src/SdJwtVcOptions.ts b/packages/sd-jwt-vc/src/SdJwtVcOptions.ts index 7f94fc6bab..6235ef17d9 100644 --- a/packages/sd-jwt-vc/src/SdJwtVcOptions.ts +++ b/packages/sd-jwt-vc/src/SdJwtVcOptions.ts @@ -1,7 +1,11 @@ import type { HashName, JwaSignatureAlgorithm } from '@aries-framework/core' -import type { DisclosureFrame } from 'jwt-sd' +import type { DisclosureFrame, PresentationFrame } from '@sd-jwt/core' -export type SdJwtVcCreateOptions = Record> = { +// TODO: extend with required claim names for input (e.g. vct) +export type SdJwtVcPayload = Record +export type SdJwtVcHeader = Record + +export type SdJwtVcCreateOptions = { holderDidUrl: string issuerDidUrl: string jsonWebAlgorithm?: JwaSignatureAlgorithm @@ -14,12 +18,12 @@ export type SdJwtVcFromSerializedJwtOptions = { holderDidUrl: string } -/** - * `includedDisclosureIndices` is not the best API, but it is the best alternative until something like `PEX` is supported - */ -export type SdJwtVcPresentOptions = { +export type SdJwtVcPresentOptions = { jsonWebAlgorithm?: JwaSignatureAlgorithm - includedDisclosureIndices?: Array + /** + * Use true to disclose everything + */ + presentationFrame: PresentationFrame | true /** * This information is received out-of-band from the verifier. @@ -32,13 +36,11 @@ export type SdJwtVcPresentOptions = { } } -/** - * `requiredClaimKeys` is not the best API, but it is the best alternative until something like `PEX` is supported - */ export type SdJwtVcVerifyOptions = { holderDidUrl: string challenge: { verifierDid: string } + // TODO: update to requiredClaimFrame requiredClaimKeys?: Array } diff --git a/packages/sd-jwt-vc/src/SdJwtVcService.ts b/packages/sd-jwt-vc/src/SdJwtVcService.ts index e411a94c25..f087720af6 100644 --- a/packages/sd-jwt-vc/src/SdJwtVcService.ts +++ b/packages/sd-jwt-vc/src/SdJwtVcService.ts @@ -4,9 +4,11 @@ import type { SdJwtVcPresentOptions, SdJwtVcFromSerializedJwtOptions, SdJwtVcVerifyOptions, + SdJwtVcPayload, + SdJwtVcHeader, } from './SdJwtVcOptions' import type { AgentContext, JwkJson, Query } from '@aries-framework/core' -import type { Signer, SdJwtVcVerificationResult, Verifier, HasherAndAlgorithm } from 'jwt-sd' +import type { Signer, SdJwtVcVerificationResult, Verifier, HasherAndAlgorithm } from '@sd-jwt/core' import { parseDid, @@ -16,14 +18,11 @@ import { Key, getJwkFromKey, Hasher, - inject, injectable, - InjectionSymbols, - Logger, TypedArrayEncoder, Buffer, } from '@aries-framework/core' -import { KeyBinding, SdJwtVc, HasherAlgorithm, Disclosure } from 'jwt-sd' +import { KeyBinding, SdJwtVc, HasherAlgorithm, Disclosure } from '@sd-jwt/core' import { SdJwtVcError } from './SdJwtVcError' import { SdJwtVcRepository, SdJwtVcRecord } from './repository' @@ -35,12 +34,10 @@ export { SdJwtVcVerificationResult } */ @injectable() export class SdJwtVcService { - private logger: Logger private sdJwtVcRepository: SdJwtVcRepository - public constructor(sdJwtVcRepository: SdJwtVcRepository, @inject(InjectionSymbols.Logger) logger: Logger) { + public constructor(sdJwtVcRepository: SdJwtVcRepository) { this.sdJwtVcRepository = sdJwtVcRepository - this.logger = logger } private async resolveDidUrl(agentContext: AgentContext, didUrl: string) { @@ -63,17 +60,14 @@ export class SdJwtVcService { /** * @todo validate the JWT header (alg) */ - private signer
= Record>( - agentContext: AgentContext, - key: Key - ): Signer
{ + private signer
(agentContext: AgentContext, key: Key): Signer
{ return async (input: string) => agentContext.wallet.sign({ key, data: TypedArrayEncoder.fromString(input) }) } /** * @todo validate the JWT header (alg) */ - private verifier
= Record>( + private verifier
( agentContext: AgentContext, signerKey: Key ): Verifier
{ @@ -99,7 +93,7 @@ export class SdJwtVcService { } } - public async signCredential = Record>( + public async signCredential( agentContext: AgentContext, sdJwtCredential: SdJwtCredential ): Promise<{ sdJwtVcRecord: SdJwtVcRecord; compact: string }> { @@ -171,7 +165,7 @@ export class SdJwtVcService { } } - public async create = Record>( + public async create( agentContext: AgentContext, payload: Payload, { @@ -181,7 +175,7 @@ export class SdJwtVcService { hashingAlgorithm = 'sha2-256', jsonWebAlgorithm, }: SdJwtVcCreateOptions - ): Promise<{ sdJwtVcRecord: SdJwtVcRecord; compact: string }> { + ) { if (hashingAlgorithm !== 'sha2-256') { throw new SdJwtVcError(`Unsupported hashing algorithm used: ${hashingAlgorithm}`) } @@ -208,7 +202,7 @@ export class SdJwtVcService { alg: alg.toString(), typ: 'vc+sd-jwt', kid: parsedDid.fragment, - } + } as const const sdJwtVc = new SdJwtVc({}, { disclosureFrame }) .withHasher(this.hasher) @@ -249,13 +243,13 @@ export class SdJwtVcService { } public async fromSerializedJwt< - Header extends Record = Record, - Payload extends Record = Record + Header extends SdJwtVcHeader = SdJwtVcHeader, + Payload extends SdJwtVcPayload = SdJwtVcPayload >( agentContext: AgentContext, sdJwtVcCompact: string, { issuerDidUrl, holderDidUrl }: SdJwtVcFromSerializedJwtOptions - ): Promise { + ): Promise> { const sdJwtVc = SdJwtVc.fromCompact(sdJwtVcCompact) let url: string | undefined @@ -288,8 +282,8 @@ export class SdJwtVcService { throw new SdJwtVcError('sd-jwt-vc has an invalid signature from the issuer') } - const { verificationMethod: holderVerificiationMethod } = await this.resolveDidUrl(agentContext, holderDidUrl) - const holderKey = getKeyFromVerificationMethod(holderVerificiationMethod) + const { verificationMethod: holderVerificationMethod } = await this.resolveDidUrl(agentContext, holderDidUrl) + const holderKey = getKeyFromVerificationMethod(holderVerificationMethod) const holderKeyJwk = getJwkFromKey(holderKey).toJson() sdJwtVc.assertClaimInPayload('cnf', { jwk: holderKeyJwk }) @@ -307,16 +301,20 @@ export class SdJwtVcService { return sdJwtVcRecord } - public async storeCredential(agentContext: AgentContext, sdJwtVcRecord: SdJwtVcRecord): Promise { + public async storeCredential(agentContext: AgentContext, sdJwtVcRecord: SdJwtVcRecord) { await this.sdJwtVcRepository.save(agentContext, sdJwtVcRecord) return sdJwtVcRecord } - public async present( + public async present< + Header extends SdJwtVcHeader = SdJwtVcHeader, + Payload extends SdJwtVcPayload = SdJwtVcPayload, + Record extends SdJwtVcRecord = SdJwtVcRecord + >( agentContext: AgentContext, - sdJwtVcRecord: SdJwtVcRecord, - { includedDisclosureIndices, verifierMetadata, jsonWebAlgorithm }: SdJwtVcPresentOptions + sdJwtVcRecord: Record, + { presentationFrame, verifierMetadata, jsonWebAlgorithm }: SdJwtVcPresentOptions ): Promise { const { verificationMethod: holderVerificationMethod } = await this.resolveDidUrl( agentContext, @@ -326,7 +324,7 @@ export class SdJwtVcService { const alg = jsonWebAlgorithm ?? getJwkFromKey(holderKey).supportedSignatureAlgorithms[0] const header = { - alg: alg.toString(), + alg, typ: 'kb+jwt', } as const @@ -334,30 +332,29 @@ export class SdJwtVcService { iat: verifierMetadata.issuedAt, nonce: verifierMetadata.nonce, aud: verifierMetadata.verifierDid, + // FIXME: _sd_hash is missing. See + // https://github.com/berendsliedrecht/sd-jwt-ts/issues/8 } - const keyBinding = new KeyBinding, Record>({ header, payload }).withSigner( - this.signer(agentContext, holderKey) - ) + const keyBinding = new KeyBinding({ header, payload }).withSigner(this.signer(agentContext, holderKey)) const sdJwtVc = new SdJwtVc({ header: sdJwtVcRecord.sdJwtVc.header, payload: sdJwtVcRecord.sdJwtVc.payload, signature: sdJwtVcRecord.sdJwtVc.signature, disclosures: sdJwtVcRecord.sdJwtVc.disclosures?.map(Disclosure.fromArray), - }).withKeyBinding(keyBinding) + }) + .withKeyBinding(keyBinding) + .withHasher(this.hasher) - return await sdJwtVc.present(includedDisclosureIndices) + return sdJwtVc.present(presentationFrame === true ? undefined : presentationFrame) } - public async verify< - Header extends Record = Record, - Payload extends Record = Record - >( + public async verify
( agentContext: AgentContext, sdJwtVcCompact: string, { challenge: { verifierDid }, requiredClaimKeys, holderDidUrl }: SdJwtVcVerifyOptions - ): Promise<{ sdJwtVcRecord: SdJwtVcRecord; validation: SdJwtVcVerificationResult }> { + ) { const sdJwtVc = SdJwtVc.fromCompact(sdJwtVcCompact) if (!sdJwtVc.signature) { @@ -390,7 +387,7 @@ export class SdJwtVcService { const verificationResult = await sdJwtVc.verify(this.verifier(agentContext, issuerKey), requiredClaimKeys) - const sdJwtVcRecord = new SdJwtVcRecord({ + const sdJwtVcRecord = new SdJwtVcRecord({ sdJwtVc: { signature: sdJwtVc.signature, payload: sdJwtVc.payload, diff --git a/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts b/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts index af262a3e73..5aac9197a2 100644 --- a/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts +++ b/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts @@ -87,7 +87,7 @@ describe('SdJwtVcService', () => { agent.context, { claim: 'some-claim', - type: 'IdentityCredential', + vct: 'IdentityCredential', }, { issuerDidUrl, @@ -105,7 +105,7 @@ describe('SdJwtVcService', () => { expect(sdJwtVcRecord.sdJwtVc.payload).toEqual({ claim: 'some-claim', - type: 'IdentityCredential', + vct: 'IdentityCredential', iat: Math.floor(new Date().getTime() / 1000), iss: issuerDidUrl.split('#')[0], cnf: { @@ -117,7 +117,7 @@ describe('SdJwtVcService', () => { test('Create sd-jwt-vc from a basic payload with a disclosure', async () => { const { compact, sdJwtVcRecord } = await sdJwtVcService.create( agent.context, - { claim: 'some-claim', type: 'IdentityCredential' }, + { claim: 'some-claim', vct: 'IdentityCredential' }, { issuerDidUrl, holderDidUrl, @@ -134,7 +134,7 @@ describe('SdJwtVcService', () => { }) expect(sdJwtVcRecord.sdJwtVc.payload).toEqual({ - type: 'IdentityCredential', + vct: 'IdentityCredential', iat: Math.floor(new Date().getTime() / 1000), iss: issuerDidUrl.split('#')[0], _sd: ['vcvFU4DsFKTqQ1vl4nelJWXTb_-0dNoBks6iqNFptyg'], @@ -155,7 +155,7 @@ describe('SdJwtVcService', () => { const { compact, sdJwtVcRecord } = await sdJwtVcService.create( agent.context, { - type: 'IdentityCredential', + vct: 'IdentityCredential', given_name: 'John', family_name: 'Doe', email: 'johndoe@example.com', @@ -195,7 +195,7 @@ describe('SdJwtVcService', () => { }) expect(sdJwtVcRecord.sdJwtVc.payload).toEqual({ - type: 'IdentityCredential', + vct: 'IdentityCredential', iat: Math.floor(new Date().getTime() / 1000), address: { _sd: ['NJnmct0BqBME1JfBlC6jRQVRuevpEONiYw7A7MHuJyQ', 'om5ZztZHB-Gd00LG21CV_xM4FaENSoiaOXnTAJNczB4'], @@ -266,7 +266,7 @@ describe('SdJwtVcService', () => { expect(sdJwtVcRecord.sdJwtVc.payload).toEqual({ claim: 'some-claim', - type: 'IdentityCredential', + vct: 'IdentityCredential', iat: Math.floor(new Date().getTime() / 1000), iss: issuerDidUrl.split('#')[0], cnf: { @@ -292,7 +292,7 @@ describe('SdJwtVcService', () => { }) expect(sdJwtVcRecord.sdJwtVc.payload).toEqual({ - type: 'IdentityCredential', + vct: 'IdentityCredential', iat: Math.floor(new Date().getTime() / 1000), iss: issuerDidUrl.split('#')[0], _sd: ['vcvFU4DsFKTqQ1vl4nelJWXTb_-0dNoBks6iqNFptyg'], @@ -326,7 +326,7 @@ describe('SdJwtVcService', () => { }) expect(sdJwtVcRecord.sdJwtVc.payload).toEqual({ - type: 'IdentityCredential', + vct: 'IdentityCredential', iat: Math.floor(new Date().getTime() / 1000), family_name: 'Doe', iss: issuerDidUrl.split('#')[0], @@ -390,6 +390,7 @@ describe('SdJwtVcService', () => { await sdJwtVcService.storeCredential(agent.context, sdJwtVcRecord) const presentation = await sdJwtVcService.present(agent.context, sdJwtVcRecord, { + presentationFrame: {}, verifierMetadata: { issuedAt: new Date().getTime() / 1000, verifierDid, @@ -411,12 +412,14 @@ describe('SdJwtVcService', () => { await sdJwtVcService.storeCredential(agent.context, sdJwtVcRecord) const presentation = await sdJwtVcService.present(agent.context, sdJwtVcRecord, { + presentationFrame: { + claim: true, + }, verifierMetadata: { issuedAt: new Date().getTime() / 1000, verifierDid, nonce: await agent.context.wallet.generateNonce(), }, - includedDisclosureIndices: [0], }) expect(presentation).toStrictEqual(sdJwtVcWithSingleDisclosurePresentation) @@ -425,7 +428,16 @@ describe('SdJwtVcService', () => { test('Present sd-jwt-vc from a basic payload with multiple (nested) disclosure', async () => { const sdJwtVc = complexSdJwtVc - const sdJwtVcRecord = await sdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { + const sdJwtVcRecord = await sdJwtVcService.fromSerializedJwt< + Record, + { + // FIXME: when not passing a payload, adding nested presentationFrame is broken + // Needs to be fixed in sd-jwt library + address: { + country: string + } + } + >(agent.context, sdJwtVc, { issuerDidUrl, holderDidUrl, }) @@ -438,7 +450,15 @@ describe('SdJwtVcService', () => { verifierDid, nonce: await agent.context.wallet.generateNonce(), }, - includedDisclosureIndices: [0, 1, 4, 6, 7], + presentationFrame: { + is_over_65: true, + is_over_21: true, + email: true, + address: { + country: true, + }, + given_name: true, + }, }) expect(presentation).toStrictEqual(complexSdJwtVcPresentation) @@ -457,6 +477,8 @@ describe('SdJwtVcService', () => { await sdJwtVcService.storeCredential(agent.context, sdJwtVcRecord) const presentation = await sdJwtVcService.present(agent.context, sdJwtVcRecord, { + // no disclosures + presentationFrame: {}, verifierMetadata: { issuedAt: new Date().getTime() / 1000, verifierDid, @@ -495,13 +517,15 @@ describe('SdJwtVcService', () => { verifierDid, nonce: await agent.context.wallet.generateNonce(), }, - includedDisclosureIndices: [0], + presentationFrame: { + claim: true, + }, }) const { validation } = await sdJwtVcService.verify(agent.context, presentation, { challenge: { verifierDid }, holderDidUrl, - requiredClaimKeys: ['type', 'cnf', 'claim', 'iat'], + requiredClaimKeys: ['vct', 'cnf', 'claim', 'iat'], }) expect(validation).toEqual({ @@ -523,20 +547,31 @@ describe('SdJwtVcService', () => { await sdJwtVcService.storeCredential(agent.context, sdJwtVcRecord) - const presentation = await sdJwtVcService.present(agent.context, sdJwtVcRecord, { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const presentation = await sdJwtVcService.present(agent.context, sdJwtVcRecord, { verifierMetadata: { issuedAt: new Date().getTime() / 1000, verifierDid, nonce: await agent.context.wallet.generateNonce(), }, - includedDisclosureIndices: [0, 1, 4, 6, 7], + presentationFrame: { + is_over_65: true, + is_over_21: true, + email: true, + address: { + country: true, + }, + given_name: true, + }, }) const { validation } = await sdJwtVcService.verify(agent.context, presentation, { challenge: { verifierDid }, holderDidUrl, + // FIXME: this should be a requiredFrame to be consistent with the other methods + // using frames requiredClaimKeys: [ - 'type', + 'vct', 'family_name', 'phone_number', 'address', diff --git a/packages/sd-jwt-vc/src/__tests__/sdjwtvc.fixtures.ts b/packages/sd-jwt-vc/src/__tests__/sdjwtvc.fixtures.ts index e345cd8c3e..fa2c77e74c 100644 --- a/packages/sd-jwt-vc/src/__tests__/sdjwtvc.fixtures.ts +++ b/packages/sd-jwt-vc/src/__tests__/sdjwtvc.fixtures.ts @@ -1,17 +1,17 @@ export const simpleJwtVc = - 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Ino2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyJ9.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ0eXBlIjoiSWRlbnRpdHlDcmVkZW50aWFsIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Im9FTlZzeE9VaUg1NFg4d0pMYVZraWNDUmswMHdCSVE0c1JnYms1NE44TW8ifX0sImlzcyI6ImRpZDprZXk6ejZNa3RxdFhORzhDRFVZOVBycnRvU3RGemVDbmhwTW1neFlMMWdpa2NXM0J6dk5XIiwiaWF0IjoxNjk4MTUxNTMyfQ.5oT776RbzRyRTINojXJExV1Ul6aP7sXKssU5bR0uWmQzVJ046y7gNhD5shJ3arYbtdakeVKBTicPM8LAzOvzAw' + 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Ino2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyJ9.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzJ9.dBWmF_E7uTHTw2t65mTJWldGZxu5_f-tOyQN7bljz1HLkqgkGuAbvx4aHlO12kTSRjs7rnwO0A79gz4CeE44Bg' export const simpleJwtVcPresentation = - 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Ino2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyJ9.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ0eXBlIjoiSWRlbnRpdHlDcmVkZW50aWFsIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Im9FTlZzeE9VaUg1NFg4d0pMYVZraWNDUmswMHdCSVE0c1JnYms1NE44TW8ifX0sImlzcyI6ImRpZDprZXk6ejZNa3RxdFhORzhDRFVZOVBycnRvU3RGemVDbmhwTW1neFlMMWdpa2NXM0J6dk5XIiwiaWF0IjoxNjk4MTUxNTMyfQ.5oT776RbzRyRTINojXJExV1Ul6aP7sXKssU5bR0uWmQzVJ046y7gNhD5shJ3arYbtdakeVKBTicPM8LAzOvzAw~eyJhbGciOiJFZERTQSIsInR5cCI6ImtiK2p3dCJ9.eyJpYXQiOjE2OTgxNTE1MzIsIm5vbmNlIjoic2FsdCIsImF1ZCI6ImRpZDprZXk6elVDNzRWRXFxaEVIUWNndjR6YWdTUGtxRkp4dU5XdW9CUEtqSnVIRVRFVWVITG9TcVd0OTJ2aVNzbWFXank4MnkifQ.VdZSnQJ5sklqMPnIzaOaGxP2qPiEPniTaUFHy4VMcW9h9pV1c17fcuTySJtmV2tcpKhei4ss04q_rFyN1EVRDg' + 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Ino2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyJ9.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzJ9.dBWmF_E7uTHTw2t65mTJWldGZxu5_f-tOyQN7bljz1HLkqgkGuAbvx4aHlO12kTSRjs7rnwO0A79gz4CeE44Bg~eyJhbGciOiJFZERTQSIsInR5cCI6ImtiK2p3dCJ9.eyJpYXQiOjE2OTgxNTE1MzIsIm5vbmNlIjoic2FsdCIsImF1ZCI6ImRpZDprZXk6elVDNzRWRXFxaEVIUWNndjR6YWdTUGtxRkp4dU5XdW9CUEtqSnVIRVRFVWVITG9TcVd0OTJ2aVNzbWFXank4MnkifQ.VdZSnQJ5sklqMPnIzaOaGxP2qPiEPniTaUFHy4VMcW9h9pV1c17fcuTySJtmV2tcpKhei4ss04q_rFyN1EVRDg' export const sdJwtVcWithSingleDisclosure = - 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Ino2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyJ9.eyJ0eXBlIjoiSWRlbnRpdHlDcmVkZW50aWFsIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Im9FTlZzeE9VaUg1NFg4d0pMYVZraWNDUmswMHdCSVE0c1JnYms1NE44TW8ifX0sImlzcyI6ImRpZDprZXk6ejZNa3RxdFhORzhDRFVZOVBycnRvU3RGemVDbmhwTW1neFlMMWdpa2NXM0J6dk5XIiwiaWF0IjoxNjk4MTUxNTMyLCJfc2RfYWxnIjoic2hhLTI1NiIsIl9zZCI6WyJ2Y3ZGVTREc0ZLVHFRMXZsNG5lbEpXWFRiXy0wZE5vQmtzNmlxTkZwdHlnIl19.G5jb2P0z-9H-AsEGBbJmGk9VUTPJJ_bkVE95oKDu4YmilmQuvCritpOoK5nt9n4Bg_3v23ywagHHOnGTBCtQCQ~WyJzYWx0IiwiY2xhaW0iLCJzb21lLWNsYWltIl0~' + 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Ino2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyJ9.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzIsIl9zZF9hbGciOiJzaGEtMjU2IiwiX3NkIjpbInZjdkZVNERzRktUcVExdmw0bmVsSldYVGJfLTBkTm9Ca3M2aXFORnB0eWciXX0.yUYqg_7fkgvh4vnoWW4L6OZpM1eAatAfKUUMhHt2xYdHtQYHdVOch1Om-mpN2lTsyw9L1sZ5KsuAx7-5T-jlDQ~WyJzYWx0IiwiY2xhaW0iLCJzb21lLWNsYWltIl0~' export const sdJwtVcWithSingleDisclosurePresentation = - 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Ino2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyJ9.eyJ0eXBlIjoiSWRlbnRpdHlDcmVkZW50aWFsIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Im9FTlZzeE9VaUg1NFg4d0pMYVZraWNDUmswMHdCSVE0c1JnYms1NE44TW8ifX0sImlzcyI6ImRpZDprZXk6ejZNa3RxdFhORzhDRFVZOVBycnRvU3RGemVDbmhwTW1neFlMMWdpa2NXM0J6dk5XIiwiaWF0IjoxNjk4MTUxNTMyLCJfc2RfYWxnIjoic2hhLTI1NiIsIl9zZCI6WyJ2Y3ZGVTREc0ZLVHFRMXZsNG5lbEpXWFRiXy0wZE5vQmtzNmlxTkZwdHlnIl19.G5jb2P0z-9H-AsEGBbJmGk9VUTPJJ_bkVE95oKDu4YmilmQuvCritpOoK5nt9n4Bg_3v23ywagHHOnGTBCtQCQ~WyJzYWx0IiwiY2xhaW0iLCJzb21lLWNsYWltIl0~eyJhbGciOiJFZERTQSIsInR5cCI6ImtiK2p3dCJ9.eyJpYXQiOjE2OTgxNTE1MzIsIm5vbmNlIjoic2FsdCIsImF1ZCI6ImRpZDprZXk6elVDNzRWRXFxaEVIUWNndjR6YWdTUGtxRkp4dU5XdW9CUEtqSnVIRVRFVWVITG9TcVd0OTJ2aVNzbWFXank4MnkifQ.VdZSnQJ5sklqMPnIzaOaGxP2qPiEPniTaUFHy4VMcW9h9pV1c17fcuTySJtmV2tcpKhei4ss04q_rFyN1EVRDg' + 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Ino2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyJ9.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzIsIl9zZF9hbGciOiJzaGEtMjU2IiwiX3NkIjpbInZjdkZVNERzRktUcVExdmw0bmVsSldYVGJfLTBkTm9Ca3M2aXFORnB0eWciXX0.yUYqg_7fkgvh4vnoWW4L6OZpM1eAatAfKUUMhHt2xYdHtQYHdVOch1Om-mpN2lTsyw9L1sZ5KsuAx7-5T-jlDQ~WyJzYWx0IiwiY2xhaW0iLCJzb21lLWNsYWltIl0~eyJhbGciOiJFZERTQSIsInR5cCI6ImtiK2p3dCJ9.eyJpYXQiOjE2OTgxNTE1MzIsIm5vbmNlIjoic2FsdCIsImF1ZCI6ImRpZDprZXk6elVDNzRWRXFxaEVIUWNndjR6YWdTUGtxRkp4dU5XdW9CUEtqSnVIRVRFVWVITG9TcVd0OTJ2aVNzbWFXank4MnkifQ.VdZSnQJ5sklqMPnIzaOaGxP2qPiEPniTaUFHy4VMcW9h9pV1c17fcuTySJtmV2tcpKhei4ss04q_rFyN1EVRDg' export const complexSdJwtVc = - 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Ino2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyJ9.eyJ0eXBlIjoiSWRlbnRpdHlDcmVkZW50aWFsIiwiZmFtaWx5X25hbWUiOiJEb2UiLCJwaG9uZV9udW1iZXIiOiIrMS0yMDItNTU1LTAxMDEiLCJhZGRyZXNzIjp7InN0cmVldF9hZGRyZXNzIjoiMTIzIE1haW4gU3QiLCJsb2NhbGl0eSI6IkFueXRvd24iLCJfc2QiOlsiTkpubWN0MEJxQk1FMUpmQmxDNmpSUVZSdWV2cEVPTmlZdzdBN01IdUp5USIsIm9tNVp6dFpIQi1HZDAwTEcyMUNWX3hNNEZhRU5Tb2lhT1huVEFKTmN6QjQiXX0sImNuZiI6eyJqd2siOnsia3R5IjoiT0tQIiwiY3J2IjoiRWQyNTUxOSIsIngiOiJvRU5Wc3hPVWlINTRYOHdKTGFWa2ljQ1JrMDB3QklRNHNSZ2JrNTROOE1vIn19LCJpc3MiOiJkaWQ6a2V5Ono2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyIsImlhdCI6MTY5ODE1MTUzMiwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsiMUN1cjJrMkEyb0lCNUNzaFNJZl9BX0tnLWwyNnVfcUt1V1E3OVAwVmRhcyIsIlIxelRVdk9ZSGdjZXBqMGpIeXBHSHo5RUh0dFZLZnQweXN3YmM5RVRQYlUiLCJlRHFRcGRUWEpYYldoZi1Fc0k3enc1WDZPdlltRk4tVVpRUU1lc1h3S1B3IiwicGREazJfWEFLSG83Z09BZndGMWI3T2RDVVZUaXQya0pIYXhTRUNROXhmYyIsInBzYXVLVU5XRWkwOW51M0NsODl4S1hnbXBXRU5abDV1eTFOMW55bl9qTWsiLCJzTl9nZTBwSFhGNnFtc1luWDFBOVNkd0o4Y2g4YUVOa3hiT0RzVDc0WXdJIl19.LcCXQx4IEnA_JWK_fLD08xXL0RWO796UuiN8YL9CU4zy_MT-LTvWJa1WNoBBeoHLcKI6NlLbXHExGU7sbG1oDw~WyJzYWx0IiwiaXNfb3Zlcl82NSIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl8yMSIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl8xOCIsdHJ1ZV0~WyJzYWx0IiwiYmlydGhkYXRlIiwiMTk0MC0wMS0wMSJd~WyJzYWx0IiwiZW1haWwiLCJqb2huZG9lQGV4YW1wbGUuY29tIl0~WyJzYWx0IiwicmVnaW9uIiwiQW55c3RhdGUiXQ~WyJzYWx0IiwiY291bnRyeSIsIlVTIl0~WyJzYWx0IiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~' + 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Ino2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyJ9.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJmYW1pbHlfbmFtZSI6IkRvZSIsInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsImFkZHJlc3MiOnsic3RyZWV0X2FkZHJlc3MiOiIxMjMgTWFpbiBTdCIsImxvY2FsaXR5IjoiQW55dG93biIsIl9zZCI6WyJOSm5tY3QwQnFCTUUxSmZCbEM2alJRVlJ1ZXZwRU9OaVl3N0E3TUh1SnlRIiwib201Wnp0WkhCLUdkMDBMRzIxQ1ZfeE00RmFFTlNvaWFPWG5UQUpOY3pCNCJdfSwiY25mIjp7Imp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Im9FTlZzeE9VaUg1NFg4d0pMYVZraWNDUmswMHdCSVE0c1JnYms1NE44TW8ifX0sImlzcyI6ImRpZDprZXk6ejZNa3RxdFhORzhDRFVZOVBycnRvU3RGemVDbmhwTW1neFlMMWdpa2NXM0J6dk5XIiwiaWF0IjoxNjk4MTUxNTMyLCJfc2RfYWxnIjoic2hhLTI1NiIsIl9zZCI6WyIxQ3VyMmsyQTJvSUI1Q3NoU0lmX0FfS2ctbDI2dV9xS3VXUTc5UDBWZGFzIiwiUjF6VFV2T1lIZ2NlcGowakh5cEdIejlFSHR0VktmdDB5c3diYzlFVFBiVSIsImVEcVFwZFRYSlhiV2hmLUVzSTd6dzVYNk92WW1GTi1VWlFRTWVzWHdLUHciLCJwZERrMl9YQUtIbzdnT0Fmd0YxYjdPZENVVlRpdDJrSkhheFNFQ1E5eGZjIiwicHNhdUtVTldFaTA5bnUzQ2w4OXhLWGdtcFdFTlpsNXV5MU4xbnluX2pNayIsInNOX2dlMHBIWEY2cW1zWW5YMUE5U2R3SjhjaDhhRU5reGJPRHNUNzRZd0kiXX0.Yz5U__nC0Nccza-NNfqhp-GueKXqeFNjm_NNtC1AJ2KdmERhCHdO6KNjM7bOiruHlo4oAlj-xObuB9LRiKXeCw~WyJzYWx0IiwiaXNfb3Zlcl82NSIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl8yMSIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl8xOCIsdHJ1ZV0~WyJzYWx0IiwiYmlydGhkYXRlIiwiMTk0MC0wMS0wMSJd~WyJzYWx0IiwiZW1haWwiLCJqb2huZG9lQGV4YW1wbGUuY29tIl0~WyJzYWx0IiwicmVnaW9uIiwiQW55c3RhdGUiXQ~WyJzYWx0IiwiY291bnRyeSIsIlVTIl0~WyJzYWx0IiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~' export const complexSdJwtVcPresentation = - 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Ino2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyJ9.eyJ0eXBlIjoiSWRlbnRpdHlDcmVkZW50aWFsIiwiZmFtaWx5X25hbWUiOiJEb2UiLCJwaG9uZV9udW1iZXIiOiIrMS0yMDItNTU1LTAxMDEiLCJhZGRyZXNzIjp7InN0cmVldF9hZGRyZXNzIjoiMTIzIE1haW4gU3QiLCJsb2NhbGl0eSI6IkFueXRvd24iLCJfc2QiOlsiTkpubWN0MEJxQk1FMUpmQmxDNmpSUVZSdWV2cEVPTmlZdzdBN01IdUp5USIsIm9tNVp6dFpIQi1HZDAwTEcyMUNWX3hNNEZhRU5Tb2lhT1huVEFKTmN6QjQiXX0sImNuZiI6eyJqd2siOnsia3R5IjoiT0tQIiwiY3J2IjoiRWQyNTUxOSIsIngiOiJvRU5Wc3hPVWlINTRYOHdKTGFWa2ljQ1JrMDB3QklRNHNSZ2JrNTROOE1vIn19LCJpc3MiOiJkaWQ6a2V5Ono2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyIsImlhdCI6MTY5ODE1MTUzMiwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsiMUN1cjJrMkEyb0lCNUNzaFNJZl9BX0tnLWwyNnVfcUt1V1E3OVAwVmRhcyIsIlIxelRVdk9ZSGdjZXBqMGpIeXBHSHo5RUh0dFZLZnQweXN3YmM5RVRQYlUiLCJlRHFRcGRUWEpYYldoZi1Fc0k3enc1WDZPdlltRk4tVVpRUU1lc1h3S1B3IiwicGREazJfWEFLSG83Z09BZndGMWI3T2RDVVZUaXQya0pIYXhTRUNROXhmYyIsInBzYXVLVU5XRWkwOW51M0NsODl4S1hnbXBXRU5abDV1eTFOMW55bl9qTWsiLCJzTl9nZTBwSFhGNnFtc1luWDFBOVNkd0o4Y2g4YUVOa3hiT0RzVDc0WXdJIl19.LcCXQx4IEnA_JWK_fLD08xXL0RWO796UuiN8YL9CU4zy_MT-LTvWJa1WNoBBeoHLcKI6NlLbXHExGU7sbG1oDw~WyJzYWx0IiwiaXNfb3Zlcl82NSIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl8yMSIsdHJ1ZV0~WyJzYWx0IiwiZW1haWwiLCJqb2huZG9lQGV4YW1wbGUuY29tIl0~WyJzYWx0IiwiY291bnRyeSIsIlVTIl0~WyJzYWx0IiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~eyJhbGciOiJFZERTQSIsInR5cCI6ImtiK2p3dCJ9.eyJpYXQiOjE2OTgxNTE1MzIsIm5vbmNlIjoic2FsdCIsImF1ZCI6ImRpZDprZXk6elVDNzRWRXFxaEVIUWNndjR6YWdTUGtxRkp4dU5XdW9CUEtqSnVIRVRFVWVITG9TcVd0OTJ2aVNzbWFXank4MnkifQ.VdZSnQJ5sklqMPnIzaOaGxP2qPiEPniTaUFHy4VMcW9h9pV1c17fcuTySJtmV2tcpKhei4ss04q_rFyN1EVRDg' + 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Ino2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyJ9.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJmYW1pbHlfbmFtZSI6IkRvZSIsInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsImFkZHJlc3MiOnsic3RyZWV0X2FkZHJlc3MiOiIxMjMgTWFpbiBTdCIsImxvY2FsaXR5IjoiQW55dG93biIsIl9zZCI6WyJOSm5tY3QwQnFCTUUxSmZCbEM2alJRVlJ1ZXZwRU9OaVl3N0E3TUh1SnlRIiwib201Wnp0WkhCLUdkMDBMRzIxQ1ZfeE00RmFFTlNvaWFPWG5UQUpOY3pCNCJdfSwiY25mIjp7Imp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Im9FTlZzeE9VaUg1NFg4d0pMYVZraWNDUmswMHdCSVE0c1JnYms1NE44TW8ifX0sImlzcyI6ImRpZDprZXk6ejZNa3RxdFhORzhDRFVZOVBycnRvU3RGemVDbmhwTW1neFlMMWdpa2NXM0J6dk5XIiwiaWF0IjoxNjk4MTUxNTMyLCJfc2RfYWxnIjoic2hhLTI1NiIsIl9zZCI6WyIxQ3VyMmsyQTJvSUI1Q3NoU0lmX0FfS2ctbDI2dV9xS3VXUTc5UDBWZGFzIiwiUjF6VFV2T1lIZ2NlcGowakh5cEdIejlFSHR0VktmdDB5c3diYzlFVFBiVSIsImVEcVFwZFRYSlhiV2hmLUVzSTd6dzVYNk92WW1GTi1VWlFRTWVzWHdLUHciLCJwZERrMl9YQUtIbzdnT0Fmd0YxYjdPZENVVlRpdDJrSkhheFNFQ1E5eGZjIiwicHNhdUtVTldFaTA5bnUzQ2w4OXhLWGdtcFdFTlpsNXV5MU4xbnluX2pNayIsInNOX2dlMHBIWEY2cW1zWW5YMUE5U2R3SjhjaDhhRU5reGJPRHNUNzRZd0kiXX0.Yz5U__nC0Nccza-NNfqhp-GueKXqeFNjm_NNtC1AJ2KdmERhCHdO6KNjM7bOiruHlo4oAlj-xObuB9LRiKXeCw~WyJzYWx0IiwiaXNfb3Zlcl82NSIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl8yMSIsdHJ1ZV0~WyJzYWx0IiwiZW1haWwiLCJqb2huZG9lQGV4YW1wbGUuY29tIl0~WyJzYWx0IiwiY291bnRyeSIsIlVTIl0~WyJzYWx0IiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~eyJhbGciOiJFZERTQSIsInR5cCI6ImtiK2p3dCJ9.eyJpYXQiOjE2OTgxNTE1MzIsIm5vbmNlIjoic2FsdCIsImF1ZCI6ImRpZDprZXk6elVDNzRWRXFxaEVIUWNndjR6YWdTUGtxRkp4dU5XdW9CUEtqSnVIRVRFVWVITG9TcVd0OTJ2aVNzbWFXank4MnkifQ.VdZSnQJ5sklqMPnIzaOaGxP2qPiEPniTaUFHy4VMcW9h9pV1c17fcuTySJtmV2tcpKhei4ss04q_rFyN1EVRDg' diff --git a/packages/sd-jwt-vc/src/repository/SdJwtVcRecord.ts b/packages/sd-jwt-vc/src/repository/SdJwtVcRecord.ts index 0075850113..ca9e2bb7c7 100644 --- a/packages/sd-jwt-vc/src/repository/SdJwtVcRecord.ts +++ b/packages/sd-jwt-vc/src/repository/SdJwtVcRecord.ts @@ -1,39 +1,38 @@ +import type { SdJwtVcHeader, SdJwtVcPayload } from '../SdJwtVcOptions' import type { TagsBase, Constructable } from '@aries-framework/core' -import type { DisclosureItem, HasherAndAlgorithm } from 'jwt-sd' +import type { DisclosureItem, HasherAndAlgorithm } from '@sd-jwt/core' import { JsonTransformer, Hasher, TypedArrayEncoder, BaseRecord, utils } from '@aries-framework/core' -import { Disclosure, HasherAlgorithm, SdJwtVc } from 'jwt-sd' +import { Disclosure, HasherAlgorithm, SdJwtVc } from '@sd-jwt/core' -export type SdJwtVcRecordTags = TagsBase & { +export type DefaultSdJwtVcRecordTags = { disclosureKeys?: Array } -export type SdJwt< - Header extends Record = Record, - Payload extends Record = Record -> = { +export type SdJwt
= { disclosures?: Array header: Header payload: Payload + // FIXME: serializing Uint8Array to and from JSON does not work. Can we just store the SD-JWT in it's compact version? signature: Uint8Array holderDidUrl: string } export type SdJwtVcRecordStorageProps< - Header extends Record = Record, - Payload extends Record = Record + Header extends SdJwtVcHeader = SdJwtVcHeader, + Payload extends SdJwtVcPayload = SdJwtVcPayload > = { id?: string createdAt?: Date - tags?: SdJwtVcRecordTags + tags?: TagsBase sdJwtVc: SdJwt } export class SdJwtVcRecord< - Header extends Record = Record, - Payload extends Record = Record -> extends BaseRecord { + Header extends SdJwtVcHeader = SdJwtVcHeader, + Payload extends SdJwtVcPayload = SdJwtVcPayload +> extends BaseRecord { public static readonly type = 'SdJwtVcRecord' public readonly type = SdJwtVcRecord.type diff --git a/packages/sd-jwt-vc/src/repository/__tests__/SdJwtVcRecord.test.ts b/packages/sd-jwt-vc/src/repository/__tests__/SdJwtVcRecord.test.ts index 5033a32974..90d47a18b9 100644 --- a/packages/sd-jwt-vc/src/repository/__tests__/SdJwtVcRecord.test.ts +++ b/packages/sd-jwt-vc/src/repository/__tests__/SdJwtVcRecord.test.ts @@ -1,5 +1,5 @@ import { JsonTransformer } from '@aries-framework/core' -import { SdJwtVc, SignatureAndEncryptionAlgorithm } from 'jwt-sd' +import { SdJwtVc, SignatureAndEncryptionAlgorithm } from '@sd-jwt/core' import { SdJwtVcRecord } from '../SdJwtVcRecord' @@ -67,7 +67,7 @@ describe('SdJwtVcRecord', () => { }, }) - const instance = JsonTransformer.fromJSON(json, SdJwtVcRecord) + const instance = JsonTransformer.deserialize(JSON.stringify(json), SdJwtVcRecord) expect(instance.type).toBe('SdJwtVcRecord') expect(instance.id).toBe('sdjwt-id') @@ -75,6 +75,7 @@ describe('SdJwtVcRecord', () => { expect(instance.getTags()).toEqual({ some: 'tag', }) + expect(instance.sdJwtVc.signature).toBeInstanceOf(Uint8Array) expect(instance.sdJwtVc).toMatchObject({ header: { alg: SignatureAndEncryptionAlgorithm.EdDSA }, payload: { iss: 'did:key:123' }, @@ -84,7 +85,7 @@ describe('SdJwtVcRecord', () => { test('Get the pretty claims', async () => { const compactSdJwtVc = - 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCJ9.eyJ0eXBlIjoiSWRlbnRpdHlDcmVkZW50aWFsIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6IlVXM3ZWRWp3UmYwSWt0Sm9jdktSbUdIekhmV0FMdF9YMkswd3ZsdVpJU3MifX0sImlzcyI6ImRpZDprZXk6MTIzIiwiaWF0IjoxNjk4MTUxNTMyLCJfc2RfYWxnIjoic2hhLTI1NiIsIl9zZCI6WyJ2Y3ZGVTREc0ZLVHFRMXZsNG5lbEpXWFRiXy0wZE5vQmtzNmlxTkZwdHlnIl19.IW6PaMTtxMNvqwrRac5nh7L9_ie4r-PUDL6Gqoey2O3axTm6RBrUv0ETLbdgALK6tU_HoIDuNE66DVrISQXaCw~WyJzYWx0IiwiY2xhaW0iLCJzb21lLWNsYWltIl0~' + 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Ino2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyJ9.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzIsIl9zZF9hbGciOiJzaGEtMjU2IiwiX3NkIjpbInZjdkZVNERzRktUcVExdmw0bmVsSldYVGJfLTBkTm9Ca3M2aXFORnB0eWciXX0.yUYqg_7fkgvh4vnoWW4L6OZpM1eAatAfKUUMhHt2xYdHtQYHdVOch1Om-mpN2lTsyw9L1sZ5KsuAx7-5T-jlDQ~WyJzYWx0IiwiY2xhaW0iLCJzb21lLWNsYWltIl0~' const sdJwtVc = SdJwtVc.fromCompact(compactSdJwtVc) @@ -105,15 +106,15 @@ describe('SdJwtVcRecord', () => { const prettyClaims = await sdJwtVcRecord.getPrettyClaims() expect(prettyClaims).toEqual({ - type: 'IdentityCredential', + vct: 'IdentityCredential', cnf: { jwk: { kty: 'OKP', crv: 'Ed25519', - x: 'UW3vVEjwRf0IktJocvKRmGHzHfWALt_X2K0wvluZISs', + x: 'oENVsxOUiH54X8wJLaVkicCRk00wBIQ4sRgbk54N8Mo', }, }, - iss: 'did:key:123', + iss: 'did:key:z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', iat: 1698151532, claim: 'some-claim', }) diff --git a/packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts b/packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts index d8c7bd086f..95ccc5f633 100644 --- a/packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts +++ b/packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts @@ -70,7 +70,7 @@ describe('sd-jwt-vc end to end test', () => { test('end to end flow', async () => { const credential = { - type: 'IdentityCredential', + vct: 'IdentityCredential', given_name: 'John', family_name: 'Doe', email: 'johndoe@example.com', @@ -85,9 +85,9 @@ describe('sd-jwt-vc end to end test', () => { is_over_18: true, is_over_21: true, is_over_65: true, - } + } as const - const { compact } = await issuer.modules.sdJwt.create(credential, { + const { compact, sdJwtVcRecord: _sdJwtVcRecord } = await issuer.modules.sdJwt.create(credential, { holderDidUrl, issuerDidUrl, disclosureFrame: { @@ -104,7 +104,10 @@ describe('sd-jwt-vc end to end test', () => { }, }) - const sdJwtVcRecord = await holder.modules.sdJwt.fromSerializedJwt(compact, { + type Payload = (typeof _sdJwtVcRecord)['sdJwtVc']['payload'] + type Header = (typeof _sdJwtVcRecord)['sdJwtVc']['header'] + + const sdJwtVcRecord = await holder.modules.sdJwt.fromSerializedJwt(compact, { issuerDidUrl, holderDidUrl, }) @@ -120,7 +123,23 @@ describe('sd-jwt-vc end to end test', () => { const presentation = await holder.modules.sdJwt.present(sdJwtVcRecord, { verifierMetadata, - includedDisclosureIndices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], + presentationFrame: { + vct: true, + given_name: true, + family_name: true, + email: true, + phone_number: true, + address: { + street_address: true, + locality: true, + region: true, + country: true, + }, + birthdate: true, + is_over_18: true, + is_over_21: true, + is_over_65: true, + }, }) const { From 9cbe4918e56a14f048b41b5289312ddbdead3183 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 8 Jan 2024 14:19:08 +0700 Subject: [PATCH 089/115] type improvement Signed-off-by: Timo Glastra --- packages/openid4vc/tests/utils.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/openid4vc/tests/utils.ts b/packages/openid4vc/tests/utils.ts index b00b02e7e7..71bb4f0093 100644 --- a/packages/openid4vc/tests/utils.ts +++ b/packages/openid4vc/tests/utils.ts @@ -1,11 +1,13 @@ -import type { KeyDidCreateOptions, ModulesMap } from '@aries-framework/core' +import type { BaseAgent, EmptyModuleMap, KeyDidCreateOptions, ModulesMap } from '@aries-framework/core' import type { TenantsModule } from '@aries-framework/tenants' -import type { TenantAgent } from '@aries-framework/tenants/src/TenantAgent' import { Agent, DidKey, KeyType, TypedArrayEncoder, utils } from '@aries-framework/core' import { agentDependencies } from '@aries-framework/node' -export async function createDidKidVerificationMethod(agent: Agent | TenantAgent, secretKey: string) { +export async function createDidKidVerificationMethod>( + agent: A, + secretKey: string +) { const didCreateResult = await agent.dids.create({ method: 'key', options: { keyType: KeyType.Ed25519 }, @@ -34,7 +36,7 @@ export async function createAgentFromModules(label: strin }) await agent.initialize() - const data = await createDidKidVerificationMethod(agent, secretKey) + const data = await createDidKidVerificationMethod(agent, secretKey) return { ...data, @@ -59,7 +61,7 @@ export async function createTenantForAgent( const secretKey = (nonce1 + nonce2).slice(0, 32) const tenant = await agent.modules.tenants.getTenantAgent({ tenantId: tenantRecord.id }) - const data = await createDidKidVerificationMethod(tenant as any, secretKey) + const data = await createDidKidVerificationMethod(tenant, secretKey) await tenant.endSession() return { From 106702ada865dc52b45eed8ffcbe9b39b0975233 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 8 Jan 2024 14:23:55 +0700 Subject: [PATCH 090/115] sd jwt updates Signed-off-by: Timo Glastra --- packages/sd-jwt-vc/src/SdJwtVcService.ts | 8 ++----- .../src/__tests__/SdJwtVcService.test.ts | 21 +++++++++++-------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/sd-jwt-vc/src/SdJwtVcService.ts b/packages/sd-jwt-vc/src/SdJwtVcService.ts index f087720af6..4d45f7c0e6 100644 --- a/packages/sd-jwt-vc/src/SdJwtVcService.ts +++ b/packages/sd-jwt-vc/src/SdJwtVcService.ts @@ -307,13 +307,9 @@ export class SdJwtVcService { return sdJwtVcRecord } - public async present< - Header extends SdJwtVcHeader = SdJwtVcHeader, - Payload extends SdJwtVcPayload = SdJwtVcPayload, - Record extends SdJwtVcRecord = SdJwtVcRecord - >( + public async present
( agentContext: AgentContext, - sdJwtVcRecord: Record, + sdJwtVcRecord: SdJwtVcRecord, { presentationFrame, verifierMetadata, jsonWebAlgorithm }: SdJwtVcPresentOptions ): Promise { const { verificationMethod: holderVerificationMethod } = await this.resolveDidUrl( diff --git a/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts b/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts index 5aac9197a2..cdd1febc7e 100644 --- a/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts +++ b/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts @@ -1,4 +1,5 @@ -import type { Key, Logger } from '@aries-framework/core' +import type { SdJwtVcHeader } from '../SdJwtVcOptions' +import type { Key } from '@aries-framework/core' import { AskarModule } from '@aries-framework/askar' import { @@ -39,7 +40,6 @@ const agent = new Agent({ dependencies: agentDependencies, }) -const logger = jest.fn() as unknown as Logger agent.context.wallet.generateNonce = jest.fn(() => Promise.resolve('salt')) Date.prototype.getTime = jest.fn(() => 1698151532000) @@ -78,7 +78,7 @@ describe('SdJwtVcService', () => { await agent.dids.import({ didDocument: holderDidDocument, did: holderDidDocument.id }) const sdJwtVcRepositoryMock = new SdJwtVcRepositoryMock() - sdJwtVcService = new SdJwtVcService(sdJwtVcRepositoryMock, logger) + sdJwtVcService = new SdJwtVcService(sdJwtVcRepositoryMock) }) describe('SdJwtVcService.create', () => { @@ -540,15 +540,18 @@ describe('SdJwtVcService', () => { test('Verify sd-jwt-vc with multiple (nested) disclosure', async () => { const sdJwtVc = complexSdJwtVc - const sdJwtVcRecord = await sdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { - issuerDidUrl, - holderDidUrl, - }) + const sdJwtVcRecord = await sdJwtVcService.fromSerializedJwt( + agent.context, + sdJwtVc, + { + issuerDidUrl, + holderDidUrl, + } + ) await sdJwtVcService.storeCredential(agent.context, sdJwtVcRecord) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const presentation = await sdJwtVcService.present(agent.context, sdJwtVcRecord, { + const presentation = await sdJwtVcService.present(agent.context, sdJwtVcRecord, { verifierMetadata: { issuedAt: new Date().getTime() / 1000, verifierDid, From 8acfb683012a9992785a44f7bd2d0a6d25d409bd Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Wed, 10 Jan 2024 16:34:46 +0700 Subject: [PATCH 091/115] more sdjwt improvements Signed-off-by: Timo Glastra --- packages/sd-jwt-vc/src/SdJwtCredential.ts | 30 - packages/sd-jwt-vc/src/SdJwtVcApi.ts | 67 +- packages/sd-jwt-vc/src/SdJwtVcOptions.ts | 69 +- packages/sd-jwt-vc/src/SdJwtVcService.ts | 590 +++++++++--------- .../src/__tests__/SdJwtVcModule.test.ts | 4 - .../src/__tests__/SdJwtVcService.test.ts | 407 ++++++------ .../src/__tests__/sdjwtvc.fixtures.ts | 12 +- packages/sd-jwt-vc/src/index.ts | 2 +- .../sd-jwt-vc/src/repository/SdJwtVcRecord.ts | 76 +-- .../__tests__/SdJwtVcRecord.test.ts | 98 +-- packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts | 128 +++- 11 files changed, 709 insertions(+), 774 deletions(-) delete mode 100644 packages/sd-jwt-vc/src/SdJwtCredential.ts diff --git a/packages/sd-jwt-vc/src/SdJwtCredential.ts b/packages/sd-jwt-vc/src/SdJwtCredential.ts deleted file mode 100644 index b518089b0a..0000000000 --- a/packages/sd-jwt-vc/src/SdJwtCredential.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { SdJwtVcPayload } from './SdJwtVcOptions' -import type { HashName, JwaSignatureAlgorithm } from '@aries-framework/core' -import type { DisclosureFrame } from '@sd-jwt/core' - -export interface SdJwtCredentialOptions { - payload: Payload - holderDidUrl: string - issuerDidUrl: string - disclosureFrame?: DisclosureFrame - jsonWebAlgorithm?: JwaSignatureAlgorithm - hashingAlgorithm?: HashName -} - -export class SdJwtCredential { - public constructor(options: SdJwtCredentialOptions) { - this.payload = options.payload - this.holderDidUrl = options.holderDidUrl - this.issuerDidUrl = options.issuerDidUrl - this.disclosureFrame = options.disclosureFrame - this.jsonWebAlgorithm = options.jsonWebAlgorithm - this.hashingAlgorithm = options.hashingAlgorithm ?? 'sha2-256' - } - - public payload: Payload - public holderDidUrl: string - public issuerDidUrl: string - public disclosureFrame?: DisclosureFrame - public jsonWebAlgorithm?: JwaSignatureAlgorithm - public hashingAlgorithm?: HashName -} diff --git a/packages/sd-jwt-vc/src/SdJwtVcApi.ts b/packages/sd-jwt-vc/src/SdJwtVcApi.ts index 0fed115410..f892f61da2 100644 --- a/packages/sd-jwt-vc/src/SdJwtVcApi.ts +++ b/packages/sd-jwt-vc/src/SdJwtVcApi.ts @@ -1,7 +1,5 @@ -import type { SdJwtCredential } from './SdJwtCredential' import type { - SdJwtVcCreateOptions, - SdJwtVcFromSerializedJwtOptions, + SdJwtVcSignOptions, SdJwtVcHeader, SdJwtVcPayload, SdJwtVcPresentOptions, @@ -27,32 +25,8 @@ export class SdJwtVcApi { this.sdJwtVcService = sdJwtVcService } - public async create(payload: Payload, options: SdJwtVcCreateOptions) { - return await this.sdJwtVcService.create(this.agentContext, payload, options) - } - - public async signCredential(credential: SdJwtCredential) { - return await this.sdJwtVcService.signCredential(this.agentContext, credential) - } - - /** - * - * Get and validate a sd-jwt-vc from a serialized JWT. - */ - public async fromSerializedJwt
( - sdJwtVcCompact: string, - options: SdJwtVcFromSerializedJwtOptions - ) { - return await this.sdJwtVcService.fromSerializedJwt(this.agentContext, sdJwtVcCompact, options) - } - - /** - * - * Stores and sd-jwt-vc record - * - */ - public async storeCredential(sdJwtVcRecord: SdJwtVcRecord) { - return await this.sdJwtVcService.storeCredential(this.agentContext, sdJwtVcRecord) + public async sign(options: SdJwtVcSignOptions) { + return await this.sdJwtVcService.sign(this.agentContext, options) } /** @@ -60,15 +34,12 @@ export class SdJwtVcApi { * Create a compact presentation of the sd-jwt. * This presentation can be send in- or out-of-band to the verifier. * - * Within the `options` field, you can supply the indicies of the disclosures you would like to share with the verifier. * Also, whether to include the holder key binding. - * */ public async present
( - sdJwtVcRecord: SdJwtVcRecord, options: SdJwtVcPresentOptions ): Promise { - return await this.sdJwtVcService.present(this.agentContext, sdJwtVcRecord, options) + return await this.sdJwtVcService.present(this.agentContext, options) } /** @@ -78,30 +49,38 @@ export class SdJwtVcApi { * For example, you might still want to continue with a flow if not all the claims are included, but the signature is valid. * */ - public async verify
( - sdJwtVcCompact: string, - options: SdJwtVcVerifyOptions - ) { - return await this.sdJwtVcService.verify(this.agentContext, sdJwtVcCompact, options) + public async verify
(options: SdJwtVcVerifyOptions) { + return await this.sdJwtVcService.verify(this.agentContext, options) + } + + /** + * Get and validate a sd-jwt-vc from a serialized JWT. + */ + public async fromCompact
(sdJwtVcCompact: string) { + return await this.sdJwtVcService.fromCompact(sdJwtVcCompact) + } + + public async store(compactSdJwtVc: string) { + return await this.sdJwtVcService.store(this.agentContext, compactSdJwtVc) } public async getById(id: string): Promise { - return await this.sdJwtVcService.getCredentialRecordById(this.agentContext, id) + return await this.sdJwtVcService.getById(this.agentContext, id) } public async getAll(): Promise> { - return await this.sdJwtVcService.getAllCredentialRecords(this.agentContext) + return await this.sdJwtVcService.getAll(this.agentContext) } public async findAllByQuery(query: Query): Promise> { - return await this.sdJwtVcService.findCredentialRecordsByQuery(this.agentContext, query) + return await this.sdJwtVcService.findByQuery(this.agentContext, query) } - public async remove(id: string) { - return await this.sdJwtVcService.removeCredentialRecord(this.agentContext, id) + public async deleteById(id: string) { + return await this.sdJwtVcService.deleteById(this.agentContext, id) } public async update(sdJwtVcRecord: SdJwtVcRecord) { - return await this.sdJwtVcService.updateCredentialRecord(this.agentContext, sdJwtVcRecord) + return await this.sdJwtVcService.update(this.agentContext, sdJwtVcRecord) } } diff --git a/packages/sd-jwt-vc/src/SdJwtVcOptions.ts b/packages/sd-jwt-vc/src/SdJwtVcOptions.ts index 6235ef17d9..87df8b03a4 100644 --- a/packages/sd-jwt-vc/src/SdJwtVcOptions.ts +++ b/packages/sd-jwt-vc/src/SdJwtVcOptions.ts @@ -1,25 +1,48 @@ -import type { HashName, JwaSignatureAlgorithm } from '@aries-framework/core' +import type { HashName, Jwk, JwkJson } from '@aries-framework/core' import type { DisclosureFrame, PresentationFrame } from '@sd-jwt/core' // TODO: extend with required claim names for input (e.g. vct) export type SdJwtVcPayload = Record export type SdJwtVcHeader = Record -export type SdJwtVcCreateOptions = { - holderDidUrl: string - issuerDidUrl: string - jsonWebAlgorithm?: JwaSignatureAlgorithm - disclosureFrame?: DisclosureFrame - hashingAlgorithm?: HashName +export interface SdJwtVcHolderDidBinding { + method: 'did' + didUrl: string +} + +export interface SdJwtVcHolderJwkBinding { + method: 'jwk' + jwk: JwkJson | Jwk +} + +export interface SdJwtVcIssuerDid { + method: 'did' + // didUrl referencing a specific key in a did document. + didUrl: string } -export type SdJwtVcFromSerializedJwtOptions = { - issuerDidUrl?: string - holderDidUrl: string +// We support jwk and did based binding for the holder at the moment +export type SdJwtVcHolderBinding = SdJwtVcHolderDidBinding | SdJwtVcHolderJwkBinding + +// We only support did based issuance currently, but we might want to add support +// for x509 or issuer metadata (as defined in SD-JWT VC) in the future +export type SdJwtVcIssuer = SdJwtVcIssuerDid + +export interface SdJwtVcSignOptions { + payload: Payload + holder: SdJwtVcHolderBinding + issuer: SdJwtVcIssuer + disclosureFrame?: DisclosureFrame + + /** + * Default of sha2-256 will be used + */ + hashingAlgorithm?: HashName } export type SdJwtVcPresentOptions = { - jsonWebAlgorithm?: JwaSignatureAlgorithm + compactSdJwtVc: string + /** * Use true to disclose everything */ @@ -30,17 +53,33 @@ export type SdJwtVcPresentOptions } diff --git a/packages/sd-jwt-vc/src/SdJwtVcService.ts b/packages/sd-jwt-vc/src/SdJwtVcService.ts index 4d45f7c0e6..fcba3f079b 100644 --- a/packages/sd-jwt-vc/src/SdJwtVcService.ts +++ b/packages/sd-jwt-vc/src/SdJwtVcService.ts @@ -1,33 +1,45 @@ -import type { SdJwtCredential } from './SdJwtCredential' import type { - SdJwtVcCreateOptions, + SdJwtVcSignOptions, SdJwtVcPresentOptions, - SdJwtVcFromSerializedJwtOptions, SdJwtVcVerifyOptions, SdJwtVcPayload, SdJwtVcHeader, + SdJwtVcHolderBinding, + SdJwtVcIssuer, } from './SdJwtVcOptions' -import type { AgentContext, JwkJson, Query } from '@aries-framework/core' -import type { Signer, SdJwtVcVerificationResult, Verifier, HasherAndAlgorithm } from '@sd-jwt/core' +import type { AgentContext, JwkJson, Query, Key } from '@aries-framework/core' +import type { Signer, SdJwtVcVerificationResult, Verifier, HasherAndAlgorithm, DisclosureItem } from '@sd-jwt/core' import { + Jwk, parseDid, DidResolverService, getKeyFromVerificationMethod, getJwkFromJson, - Key, getJwkFromKey, Hasher, injectable, TypedArrayEncoder, Buffer, } from '@aries-framework/core' -import { KeyBinding, SdJwtVc, HasherAlgorithm, Disclosure } from '@sd-jwt/core' +import { KeyBinding, SdJwtVc as _SdJwtVc, HasherAlgorithm } from '@sd-jwt/core' import { SdJwtVcError } from './SdJwtVcError' -import { SdJwtVcRepository, SdJwtVcRecord } from './repository' +import { SdJwtVcRecord, SdJwtVcRepository } from './repository' -export { SdJwtVcVerificationResult } +export { SdJwtVcVerificationResult, DisclosureItem } + +export interface SdJwtVc< + Header extends SdJwtVcHeader = SdJwtVcHeader, + Payload extends SdJwtVcPayload = SdJwtVcPayload +> { + compact: string + header: Header + + // TODO: payload type here is a lie, as it is the signed payload (so fields replaced with _sd) + payload: Payload + prettyClaims: Payload +} /** * @internal @@ -40,387 +52,357 @@ export class SdJwtVcService { this.sdJwtVcRepository = sdJwtVcRepository } - private async resolveDidUrl(agentContext: AgentContext, didUrl: string) { - const didResolver = agentContext.dependencyManager.resolve(DidResolverService) - const didDocument = await didResolver.resolveDidDocument(agentContext, didUrl) - - return { verificationMethod: didDocument.dereferenceKey(didUrl), didDocument } - } - - private get hasher(): HasherAndAlgorithm { - return { - algorithm: HasherAlgorithm.Sha256, - hasher: (input: string) => { - const serializedInput = TypedArrayEncoder.fromString(input) - return Hasher.hash(serializedInput, 'sha2-256') - }, - } - } - - /** - * @todo validate the JWT header (alg) - */ - private signer
(agentContext: AgentContext, key: Key): Signer
{ - return async (input: string) => agentContext.wallet.sign({ key, data: TypedArrayEncoder.fromString(input) }) - } - - /** - * @todo validate the JWT header (alg) - */ - private verifier
( - agentContext: AgentContext, - signerKey: Key - ): Verifier
{ - return async ({ message, signature, publicKeyJwk }) => { - let key = signerKey - - if (publicKeyJwk) { - if (!('kty' in publicKeyJwk)) { - throw new SdJwtVcError( - 'Key type (kty) claim could not be found in the JWK of the confirmation (cnf) claim. Only JWK is supported right now' - ) - } - - const jwk = getJwkFromJson(publicKeyJwk as JwkJson) - key = Key.fromPublicKey(jwk.publicKey, jwk.keyType) - } - - return await agentContext.wallet.verify({ - signature: Buffer.from(signature), - key: key, - data: TypedArrayEncoder.fromString(message), - }) - } - } - - public async signCredential( - agentContext: AgentContext, - sdJwtCredential: SdJwtCredential - ): Promise<{ sdJwtVcRecord: SdJwtVcRecord; compact: string }> { - const { holderDidUrl, issuerDidUrl, payload, disclosureFrame, hashingAlgorithm, jsonWebAlgorithm } = sdJwtCredential - - if (hashingAlgorithm !== 'sha2-256') { - throw new SdJwtVcError(`Unsupported hashing algorithm used: ${hashingAlgorithm}`) - } - - const parsedDid = parseDid(issuerDidUrl) - if (!parsedDid.fragment) { - throw new SdJwtVcError( - `issuer did url '${issuerDidUrl}' does not contain a '#'. Unable to derive key from did document` - ) - } - - const { verificationMethod: issuerVerificationMethod, didDocument: issuerDidDocument } = await this.resolveDidUrl( - agentContext, - issuerDidUrl - ) - const issuerKey = getKeyFromVerificationMethod(issuerVerificationMethod) - const alg = jsonWebAlgorithm ?? getJwkFromKey(issuerKey).supportedSignatureAlgorithms[0] - - const { verificationMethod: holderVerificationMethod } = await this.resolveDidUrl(agentContext, holderDidUrl) - const holderKey = getKeyFromVerificationMethod(holderVerificationMethod) - const holderKeyJwk = getJwkFromKey(holderKey).toJson() - - const header = { - alg: alg.toString(), - typ: 'vc+sd-jwt', - kid: parsedDid.fragment, - } - - const sdJwtVc = new SdJwtVc({}, { disclosureFrame }) - .withHasher(this.hasher) - .withSigner(this.signer(agentContext, issuerKey)) - .withSaltGenerator(agentContext.wallet.generateNonce) - .withHeader(header) - .withPayload({ ...payload }) + public async sign(agentContext: AgentContext, options: SdJwtVcSignOptions) { + const { payload, disclosureFrame, hashingAlgorithm } = options - // Add the `cnf` claim for the holder key binding - sdJwtVc.addPayloadClaim('cnf', { jwk: holderKeyJwk }) - - // Add the issuer DID as the `iss` claim - sdJwtVc.addPayloadClaim('iss', issuerDidDocument.id) - - // Add the issued at (iat) claim - sdJwtVc.addPayloadClaim('iat', Math.floor(new Date().getTime() / 1000)) - - const compact = await sdJwtVc.toCompact() - - if (!sdJwtVc.signature) { - throw new SdJwtVcError('Invalid sd-jwt-vc state. Signature should have been set when calling `toCompact`.') - } - - const sdJwtVcRecord = new SdJwtVcRecord({ - sdJwtVc: { - header: sdJwtVc.header, - payload: sdJwtVc.payload, - signature: sdJwtVc.signature, - disclosures: sdJwtVc.disclosures?.map((d) => d.decoded), - holderDidUrl, - }, - }) - - return { - sdJwtVcRecord, - compact, - } - } - - public async create( - agentContext: AgentContext, - payload: Payload, - { - issuerDidUrl, - holderDidUrl, - disclosureFrame, - hashingAlgorithm = 'sha2-256', - jsonWebAlgorithm, - }: SdJwtVcCreateOptions - ) { - if (hashingAlgorithm !== 'sha2-256') { + // default is sha2-256 + if (hashingAlgorithm && hashingAlgorithm !== 'sha2-256') { throw new SdJwtVcError(`Unsupported hashing algorithm used: ${hashingAlgorithm}`) } - const parsedDid = parseDid(issuerDidUrl) - if (!parsedDid.fragment) { - throw new SdJwtVcError( - `issuer did url '${issuerDidUrl}' does not contain a '#'. Unable to derive key from did document` - ) - } - - const { verificationMethod: issuerVerificationMethod, didDocument: issuerDidDocument } = await this.resolveDidUrl( - agentContext, - issuerDidUrl - ) - const issuerKey = getKeyFromVerificationMethod(issuerVerificationMethod) - const alg = jsonWebAlgorithm ?? getJwkFromKey(issuerKey).supportedSignatureAlgorithms[0] - - const { verificationMethod: holderVerificationMethod } = await this.resolveDidUrl(agentContext, holderDidUrl) - const holderKey = getKeyFromVerificationMethod(holderVerificationMethod) - const holderKeyJwk = getJwkFromKey(holderKey).toJson() + const issuer = await this.extractKeyFromIssuer(agentContext, options.issuer) + const holderBinding = await this.extractKeyFromHolderBinding(agentContext, options.holder) const header = { - alg: alg.toString(), + alg: issuer.alg, typ: 'vc+sd-jwt', - kid: parsedDid.fragment, + kid: issuer.kid, } as const - const sdJwtVc = new SdJwtVc({}, { disclosureFrame }) + const sdJwtVc = new _SdJwtVc({}, { disclosureFrame }) .withHasher(this.hasher) - .withSigner(this.signer(agentContext, issuerKey)) + .withSigner(this.signer(agentContext, issuer.key)) .withSaltGenerator(agentContext.wallet.generateNonce) .withHeader(header) .withPayload({ ...payload }) // Add the `cnf` claim for the holder key binding - sdJwtVc.addPayloadClaim('cnf', { jwk: holderKeyJwk }) + sdJwtVc.addPayloadClaim('cnf', holderBinding.cnf) - // Add the issuer DID as the `iss` claim - sdJwtVc.addPayloadClaim('iss', issuerDidDocument.id) + // Add `iss` claim + sdJwtVc.addPayloadClaim('iss', issuer.iss) // Add the issued at (iat) claim sdJwtVc.addPayloadClaim('iat', Math.floor(new Date().getTime() / 1000)) const compact = await sdJwtVc.toCompact() - if (!sdJwtVc.signature) { throw new SdJwtVcError('Invalid sd-jwt-vc state. Signature should have been set when calling `toCompact`.') } - const sdJwtVcRecord = new SdJwtVcRecord({ - sdJwtVc: { - header: sdJwtVc.header, - payload: sdJwtVc.payload, - signature: sdJwtVc.signature, - disclosures: sdJwtVc.disclosures?.map((d) => d.decoded), - holderDidUrl, - }, - }) - return { - sdJwtVcRecord, compact, - } + prettyClaims: await sdJwtVc.getPrettyClaims(), + header: sdJwtVc.header, + payload: sdJwtVc.payload, + } satisfies SdJwtVc } - public async fromSerializedJwt< + public async fromCompact< Header extends SdJwtVcHeader = SdJwtVcHeader, Payload extends SdJwtVcPayload = SdJwtVcPayload - >( - agentContext: AgentContext, - sdJwtVcCompact: string, - { issuerDidUrl, holderDidUrl }: SdJwtVcFromSerializedJwtOptions - ): Promise> { - const sdJwtVc = SdJwtVc.fromCompact(sdJwtVcCompact) - - let url: string | undefined - if (issuerDidUrl) { - url = issuerDidUrl - } else { - const iss = sdJwtVc.payload?.iss - if (typeof iss === 'string' && iss.startsWith('did')) { - const kid = sdJwtVc.header?.kid - if (!kid || typeof kid !== 'string') throw new SdJwtVcError(`Missing 'kid' in header of SdJwtVc.`) - if (kid.startsWith('did:')) url = kid - else url = `${iss}#${kid}` - } else if (typeof iss === 'string' && URL.canParse(iss)) { - throw new SdJwtVcError(`Resolving the key material from the 'iss' claim is not supported yet.`) - } else { - throw new SdJwtVcError(`Invalid iss claim '${iss}' in SdJwtVc.`) - } - } + >(compactSdJwtVc: string): Promise> { + const sdJwtVc = _SdJwtVc.fromCompact(compactSdJwtVc).withHasher(this.hasher) if (!sdJwtVc.signature) { throw new SdJwtVcError('A signature must be included for an sd-jwt-vc') } - const { verificationMethod: issuerVerificationMethod } = await this.resolveDidUrl(agentContext, url) - const issuerKey = getKeyFromVerificationMethod(issuerVerificationMethod) - - const { isSignatureValid } = await sdJwtVc.verify(this.verifier(agentContext, issuerKey)) + return { + compact: compactSdJwtVc, + header: sdJwtVc.header, + payload: sdJwtVc.payload, - if (!isSignatureValid) { - throw new SdJwtVcError('sd-jwt-vc has an invalid signature from the issuer') + prettyClaims: await sdJwtVc.getPrettyClaims(), } - - const { verificationMethod: holderVerificationMethod } = await this.resolveDidUrl(agentContext, holderDidUrl) - const holderKey = getKeyFromVerificationMethod(holderVerificationMethod) - const holderKeyJwk = getJwkFromKey(holderKey).toJson() - - sdJwtVc.assertClaimInPayload('cnf', { jwk: holderKeyJwk }) - - const sdJwtVcRecord = new SdJwtVcRecord({ - sdJwtVc: { - header: sdJwtVc.header, - payload: sdJwtVc.payload, - signature: sdJwtVc.signature, - disclosures: sdJwtVc.disclosures?.map((d) => d.decoded), - holderDidUrl, - }, - }) - - return sdJwtVcRecord - } - - public async storeCredential(agentContext: AgentContext, sdJwtVcRecord: SdJwtVcRecord) { - await this.sdJwtVcRepository.save(agentContext, sdJwtVcRecord) - - return sdJwtVcRecord } public async present
( agentContext: AgentContext, - sdJwtVcRecord: SdJwtVcRecord, - { presentationFrame, verifierMetadata, jsonWebAlgorithm }: SdJwtVcPresentOptions + { compactSdJwtVc, presentationFrame, verifierMetadata }: SdJwtVcPresentOptions ): Promise { - const { verificationMethod: holderVerificationMethod } = await this.resolveDidUrl( - agentContext, - sdJwtVcRecord.sdJwtVc.holderDidUrl - ) - const holderKey = getKeyFromVerificationMethod(holderVerificationMethod) - const alg = jsonWebAlgorithm ?? getJwkFromKey(holderKey).supportedSignatureAlgorithms[0] + const sdJwtVc = _SdJwtVc.fromCompact(compactSdJwtVc).withHasher(this.hasher) + const holder = await this.extractKeyFromHolderBinding(agentContext, this.parseHolderBindingFromCredential(sdJwtVc)) + + // FIXME: we create the SD-JWT in two steps as the _sd_hash is currently not included in the SD-JWT library + // so we add it ourselves, but for that we need the contents of the derived SD-JWT first + let compactDerivedSdJwtVc = await sdJwtVc.present(presentationFrame === true ? undefined : presentationFrame) + // FIXME: can be removed once https://github.com/berendsliedrecht/sd-jwt-ts/pull/19 is released + if (!compactDerivedSdJwtVc.endsWith('~')) { + compactDerivedSdJwtVc = `${compactDerivedSdJwtVc}~` + } const header = { - alg, + alg: holder.alg, typ: 'kb+jwt', } as const const payload = { iat: verifierMetadata.issuedAt, nonce: verifierMetadata.nonce, - aud: verifierMetadata.verifierDid, + aud: verifierMetadata.audience, + // FIXME: _sd_hash is missing. See // https://github.com/berendsliedrecht/sd-jwt-ts/issues/8 + _sd_hash: TypedArrayEncoder.toBase64URL(await this.hasher.hasher(compactDerivedSdJwtVc)), } - const keyBinding = new KeyBinding({ header, payload }).withSigner(this.signer(agentContext, holderKey)) + const compactKbJwt = await new KeyBinding({ header, payload }) + .withSigner(this.signer(agentContext, holder.key)) + .toCompact() - const sdJwtVc = new SdJwtVc({ - header: sdJwtVcRecord.sdJwtVc.header, - payload: sdJwtVcRecord.sdJwtVc.payload, - signature: sdJwtVcRecord.sdJwtVc.signature, - disclosures: sdJwtVcRecord.sdJwtVc.disclosures?.map(Disclosure.fromArray), - }) - .withKeyBinding(keyBinding) - .withHasher(this.hasher) - - return sdJwtVc.present(presentationFrame === true ? undefined : presentationFrame) + return `${compactDerivedSdJwtVc}${compactKbJwt}` } public async verify
( agentContext: AgentContext, - sdJwtVcCompact: string, - { challenge: { verifierDid }, requiredClaimKeys, holderDidUrl }: SdJwtVcVerifyOptions + { compactSdJwtVc, keyBinding, requiredClaimKeys }: SdJwtVcVerifyOptions ) { - const sdJwtVc = SdJwtVc.fromCompact(sdJwtVcCompact) + const sdJwtVc = _SdJwtVc.fromCompact(compactSdJwtVc) + + const issuer = await this.extractKeyFromIssuer(agentContext, this.parseIssuerFromCredential(sdJwtVc)) + const holder = await this.extractKeyFromHolderBinding(agentContext, this.parseHolderBindingFromCredential(sdJwtVc)) + + // FIXME: sdJwtVc library must support passing a custom jwk resolver based on the cnf claim + // or passing in the resolved key already. Currently the implementation assumes the cnf is always + // a jwk. But this won't work if we want to bind the cnf to a did + const verificationResult = await sdJwtVc.verify( + this.verifier(agentContext, issuer.key), + requiredClaimKeys, + holder.cnf + ) - if (!sdJwtVc.signature) { - throw new SdJwtVcError('A signature is required for verification of the sd-jwt-vc') - } + // If keyBinding is present, verify the key binding + try { + if (keyBinding) { + if (!sdJwtVc.keyBinding || !sdJwtVc.keyBinding.payload) { + throw new SdJwtVcError('Keybinding is required for verification of the sd-jwt-vc') + } - if (!sdJwtVc.keyBinding || !sdJwtVc.keyBinding.payload) { - throw new SdJwtVcError('Keybinding is required for verification of the sd-jwt-vc') + // FIXME: Calculate _sd_hash. can be removed once below is resolved + // https://github.com/berendsliedrecht/sd-jwt-ts/issues/8 + const sdJwtParts = compactSdJwtVc.split('~') + sdJwtParts.pop() // remove kb-jwt + const sdJwtWithoutKbJwt = `${sdJwtParts.join('~')}~` + const sdHash = TypedArrayEncoder.toBase64URL(await this.hasher.hasher(sdJwtWithoutKbJwt)) + + // Assert `aud` and `nonce` claims + sdJwtVc.keyBinding.assertClaimInPayload('aud', keyBinding.audience) + sdJwtVc.keyBinding.assertClaimInPayload('nonce', keyBinding.nonce) + sdJwtVc.keyBinding.assertClaimInPayload('_sd_hash', sdHash) + } + } catch (error) { + verificationResult.isKeyBindingValid = false + verificationResult.isValid = false } - sdJwtVc.keyBinding.assertClaimInPayload('aud', verifierDid) + return { + verification: verificationResult, + } + } - const { verificationMethod: holderVerificationMethod } = await this.resolveDidUrl(agentContext, holderDidUrl) - const holderKey = getKeyFromVerificationMethod(holderVerificationMethod) - const holderKeyJwk = getJwkFromKey(holderKey).toJson() + public async store(agentContext: AgentContext, compactSdJwtVc: string) { + const sdJwtVcRecord = new SdJwtVcRecord({ + compactSdJwtVc, + }) + await this.sdJwtVcRepository.save(agentContext, sdJwtVcRecord) - sdJwtVc.assertClaimInPayload('cnf', { jwk: holderKeyJwk }) + return sdJwtVcRecord + } - sdJwtVc.assertClaimInHeader('kid') - sdJwtVc.assertClaimInPayload('iss') + public async getById(agentContext: AgentContext, id: string): Promise { + return await this.sdJwtVcRepository.getById(agentContext, id) + } - const issuerKid = sdJwtVc.getClaimInHeader('kid') - const issuerDid = sdJwtVc.getClaimInPayload('iss') + public async getAll(agentContext: AgentContext): Promise> { + return await this.sdJwtVcRepository.getAll(agentContext) + } - // TODO: is there a more AFJ way of doing this? - const issuerDidUrl = `${issuerDid}#${issuerKid}` + public async findByQuery(agentContext: AgentContext, query: Query): Promise> { + return await this.sdJwtVcRepository.findByQuery(agentContext, query) + } - const { verificationMethod: issuerVerificationMethod } = await this.resolveDidUrl(agentContext, issuerDidUrl) - const issuerKey = getKeyFromVerificationMethod(issuerVerificationMethod) + public async deleteById(agentContext: AgentContext, id: string) { + await this.sdJwtVcRepository.deleteById(agentContext, id) + } - const verificationResult = await sdJwtVc.verify(this.verifier(agentContext, issuerKey), requiredClaimKeys) + public async update(agentContext: AgentContext, sdJwtVcRecord: SdJwtVcRecord) { + await this.sdJwtVcRepository.update(agentContext, sdJwtVcRecord) + } - const sdJwtVcRecord = new SdJwtVcRecord({ - sdJwtVc: { - signature: sdJwtVc.signature, - payload: sdJwtVc.payload, - disclosures: sdJwtVc.disclosures?.map((d) => d.decoded), - header: sdJwtVc.header, - holderDidUrl, - }, - }) + private async resolveDidUrl(agentContext: AgentContext, didUrl: string) { + const didResolver = agentContext.dependencyManager.resolve(DidResolverService) + const didDocument = await didResolver.resolveDidDocument(agentContext, didUrl) - await this.sdJwtVcRepository.save(agentContext, sdJwtVcRecord) + return { verificationMethod: didDocument.dereferenceKey(didUrl), didDocument } + } + private get hasher(): HasherAndAlgorithm { return { - sdJwtVcRecord, - validation: verificationResult, + algorithm: HasherAlgorithm.Sha256, + hasher: (input: string) => { + const serializedInput = TypedArrayEncoder.fromString(input) + return Hasher.hash(serializedInput, 'sha2-256') + }, } } - public async getCredentialRecordById(agentContext: AgentContext, id: string): Promise { - return await this.sdJwtVcRepository.getById(agentContext, id) + /** + * @todo validate the JWT header (alg) + */ + private signer
(agentContext: AgentContext, key: Key): Signer
{ + return async (input: string) => agentContext.wallet.sign({ key, data: TypedArrayEncoder.fromString(input) }) } - public async getAllCredentialRecords(agentContext: AgentContext): Promise> { - return await this.sdJwtVcRepository.getAll(agentContext) + /** + * @todo validate the JWT header (alg) + * FIXME: also support kid (did) for cnf claim + */ + private verifier
( + agentContext: AgentContext, + signerKey: Key + ): Verifier
{ + return async ({ message, signature, publicKeyJwk }) => { + let key = signerKey + + if (publicKeyJwk) { + if (!('kty' in publicKeyJwk)) { + throw new SdJwtVcError( + 'Key type (kty) claim could not be found in the JWK of the confirmation (cnf) claim. Only JWK is supported right now' + ) + } + + key = getJwkFromJson(publicKeyJwk as JwkJson).key + } + + return await agentContext.wallet.verify({ + signature: Buffer.from(signature), + key: key, + data: TypedArrayEncoder.fromString(message), + }) + } } - public async findCredentialRecordsByQuery( - agentContext: AgentContext, - query: Query - ): Promise> { - return await this.sdJwtVcRepository.findByQuery(agentContext, query) + private async extractKeyFromIssuer(agentContext: AgentContext, issuer: SdJwtVcIssuer) { + if (issuer.method === 'did') { + const parsedDid = parseDid(issuer.didUrl) + if (!parsedDid.fragment) { + throw new SdJwtVcError( + `didUrl '${issuer.didUrl}' does not contain a '#'. Unable to derive key from did document` + ) + } + + const { verificationMethod } = await this.resolveDidUrl(agentContext, issuer.didUrl) + const key = getKeyFromVerificationMethod(verificationMethod) + const alg = getJwkFromKey(key).supportedSignatureAlgorithms[0] + + return { + alg, + key, + iss: parsedDid.did, + kid: `#${parsedDid.fragment}`, + } + } + + throw new SdJwtVcError("Unsupported credential issuer. Only 'did' is supported at the moment.") } - public async removeCredentialRecord(agentContext: AgentContext, id: string) { - await this.sdJwtVcRepository.deleteById(agentContext, id) + private parseIssuerFromCredential
( + sdJwtVc: _SdJwtVc + ): SdJwtVcIssuer { + const iss = sdJwtVc.getClaimInPayload('iss') + + if (iss.startsWith('did:')) { + // If `did` is used, we require a relative KID to be present to identify + // the key used by issuer to sign the sd-jwt-vc + sdJwtVc.assertClaimInHeader('kid') + const issuerKid = sdJwtVc.getClaimInHeader('kid') + + let didUrl: string + if (issuerKid.startsWith('#')) { + didUrl = `${iss}${issuerKid}` + } else if (issuerKid.startsWith('did:')) { + const didFromKid = parseDid(issuerKid) + if (didFromKid.did !== iss) { + throw new SdJwtVcError( + `kid in header is an absolute DID URL, but the did (${didFromKid.did}) does not match with the 'iss' did (${iss})` + ) + } + + didUrl = issuerKid + } else { + throw new SdJwtVcError( + 'Invalid issuer kid for did. Only absolute or relative (starting with #) did urls are supported.' + ) + } + + return { + method: 'did', + didUrl, + } + } + throw new SdJwtVcError("Unsupported 'iss' value. Only did is supported at the moment.") } - public async updateCredentialRecord(agentContext: AgentContext, sdJwtVcRecord: SdJwtVcRecord) { - await this.sdJwtVcRepository.update(agentContext, sdJwtVcRecord) + private parseHolderBindingFromCredential
( + sdJwtVc: _SdJwtVc + ): SdJwtVcHolderBinding { + const cnf = sdJwtVc.getClaimInPayload<{ jwk?: JwkJson; kid?: string }>('cnf') + + if (cnf.jwk) { + return { + method: 'jwk', + jwk: cnf.jwk, + } + } else if (cnf.kid) { + if (!cnf.kid.startsWith('did:') || !cnf.kid.includes('#')) { + throw new SdJwtVcError('Invalid holder kid for did. Only absolute KIDs for cnf are supported') + } + return { + method: 'did', + didUrl: cnf.kid, + } + } + + throw new SdJwtVcError("Unsupported credential holder binding. Only 'did' and 'jwk' are supported at the moment.") + } + + private async extractKeyFromHolderBinding(agentContext: AgentContext, holder: SdJwtVcHolderBinding) { + if (holder.method === 'did') { + const parsedDid = parseDid(holder.didUrl) + if (!parsedDid.fragment) { + throw new SdJwtVcError( + `didUrl '${holder.didUrl}' does not contain a '#'. Unable to derive key from did document` + ) + } + + const { verificationMethod } = await this.resolveDidUrl(agentContext, holder.didUrl) + const key = getKeyFromVerificationMethod(verificationMethod) + const alg = getJwkFromKey(key).supportedSignatureAlgorithms[0] + + return { + alg, + key, + cnf: { + // We need to include the whole didUrl here, otherwise the verifier + // won't know which did it is associated with + kid: holder.didUrl, + }, + } + } else if (holder.method === 'jwk') { + const jwk = holder.jwk instanceof Jwk ? holder.jwk : getJwkFromJson(holder.jwk) + const key = jwk.key + const alg = jwk.supportedSignatureAlgorithms[0] + + return { + alg, + key, + cnf: { + jwk: jwk.toJson(), + }, + } + } + + throw new SdJwtVcError("Unsupported credential holder binding. Only 'did' and 'jwk' are supported at the moment.") } } diff --git a/packages/sd-jwt-vc/src/__tests__/SdJwtVcModule.test.ts b/packages/sd-jwt-vc/src/__tests__/SdJwtVcModule.test.ts index 0adc239614..8a58e9c512 100644 --- a/packages/sd-jwt-vc/src/__tests__/SdJwtVcModule.test.ts +++ b/packages/sd-jwt-vc/src/__tests__/SdJwtVcModule.test.ts @@ -1,6 +1,5 @@ import type { DependencyManager } from '@aries-framework/core' -import { SdJwtVcApi } from '../SdJwtVcApi' import { SdJwtVcModule } from '../SdJwtVcModule' import { SdJwtVcService } from '../SdJwtVcService' import { SdJwtVcRepository } from '../repository' @@ -17,9 +16,6 @@ describe('SdJwtVcModule', () => { const sdJwtVcModule = new SdJwtVcModule() sdJwtVcModule.register(dependencyManager) - expect(dependencyManager.registerContextScoped).toHaveBeenCalledTimes(1) - expect(dependencyManager.registerContextScoped).toHaveBeenCalledWith(SdJwtVcApi) - expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(2) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(SdJwtVcService) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(SdJwtVcRepository) diff --git a/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts b/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts index cdd1febc7e..4bf3dbffe5 100644 --- a/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts +++ b/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts @@ -1,8 +1,9 @@ import type { SdJwtVcHeader } from '../SdJwtVcOptions' -import type { Key } from '@aries-framework/core' +import type { Jwk, Key } from '@aries-framework/core' import { AskarModule } from '@aries-framework/askar' import { + parseDid, getJwkFromKey, DidKey, DidsModule, @@ -28,6 +29,12 @@ import { simpleJwtVcPresentation, } from './sdjwtvc.fixtures' +const jwkJsonWithoutUse = (jwk: Jwk) => { + const jwkJson = jwk.toJson() + delete jwkJson.use + return jwkJson +} + const agent = new Agent({ config: { label: 'sdjwtvcserviceagent', walletConfig: { id: utils.uuid(), key: utils.uuid() } }, modules: { @@ -49,7 +56,6 @@ const SdJwtVcRepositoryMock = SdJwtVcRepository as jest.Mock describe('SdJwtVcService', () => { const verifierDid = 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y' let issuerDidUrl: string - let holderDidUrl: string let issuerKey: Key let holderKey: Key let sdJwtVcService: SdJwtVcService @@ -74,87 +80,108 @@ describe('SdJwtVcService', () => { const holderDidKey = new DidKey(holderKey) const holderDidDocument = holderDidKey.didDocument - holderDidUrl = (holderDidDocument.verificationMethod ?? [])[0].id await agent.dids.import({ didDocument: holderDidDocument, did: holderDidDocument.id }) const sdJwtVcRepositoryMock = new SdJwtVcRepositoryMock() sdJwtVcService = new SdJwtVcService(sdJwtVcRepositoryMock) }) - describe('SdJwtVcService.create', () => { - test('Create sd-jwt-vc from a basic payload without disclosures', async () => { - const { compact, sdJwtVcRecord } = await sdJwtVcService.create( - agent.context, - { + describe('SdJwtVcService.sign', () => { + test('Sign sd-jwt-vc from a basic payload without disclosures', async () => { + const { compact } = await sdJwtVcService.sign(agent.context, { + payload: { claim: 'some-claim', vct: 'IdentityCredential', }, - { - issuerDidUrl, - holderDidUrl, - } - ) + holder: { + // FIXME: is it nicer API to just pass either didUrl or JWK? + // Or none if you don't want to bind it? + method: 'jwk', + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + }, + issuer: { + method: 'did', + didUrl: issuerDidUrl, + }, + }) expect(compact).toStrictEqual(simpleJwtVc) - expect(sdJwtVcRecord.sdJwtVc.header).toEqual({ + const sdJwtVc = await sdJwtVcService.fromCompact(compact) + + expect(sdJwtVc.header).toEqual({ alg: 'EdDSA', typ: 'vc+sd-jwt', - kid: 'z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + kid: '#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', }) - expect(sdJwtVcRecord.sdJwtVc.payload).toEqual({ + expect(sdJwtVc.prettyClaims).toEqual({ claim: 'some-claim', vct: 'IdentityCredential', iat: Math.floor(new Date().getTime() / 1000), - iss: issuerDidUrl.split('#')[0], + iss: parseDid(issuerDidUrl).did, cnf: { - jwk: getJwkFromKey(holderKey).toJson(), + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), }, }) }) test('Create sd-jwt-vc from a basic payload with a disclosure', async () => { - const { compact, sdJwtVcRecord } = await sdJwtVcService.create( - agent.context, - { claim: 'some-claim', vct: 'IdentityCredential' }, - { - issuerDidUrl, - holderDidUrl, - disclosureFrame: { claim: true }, - } - ) + const { compact, header, prettyClaims, payload } = await sdJwtVcService.sign(agent.context, { + payload: { claim: 'some-claim', vct: 'IdentityCredential' }, + disclosureFrame: { claim: true }, + holder: { + method: 'jwk', + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + }, + issuer: { + method: 'did', + didUrl: issuerDidUrl, + }, + }) expect(compact).toStrictEqual(sdJwtVcWithSingleDisclosure) - expect(sdJwtVcRecord.sdJwtVc.header).toEqual({ + expect(header).toEqual({ alg: 'EdDSA', typ: 'vc+sd-jwt', - kid: 'z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + kid: '#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', }) - expect(sdJwtVcRecord.sdJwtVc.payload).toEqual({ + expect(payload).toEqual({ vct: 'IdentityCredential', iat: Math.floor(new Date().getTime() / 1000), iss: issuerDidUrl.split('#')[0], _sd: ['vcvFU4DsFKTqQ1vl4nelJWXTb_-0dNoBks6iqNFptyg'], _sd_alg: 'sha-256', cnf: { - jwk: getJwkFromKey(holderKey).toJson(), + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), }, }) - expect(sdJwtVcRecord.sdJwtVc.payload).not.toContain({ + expect(prettyClaims).toEqual({ + vct: 'IdentityCredential', + iat: Math.floor(new Date().getTime() / 1000), + iss: issuerDidUrl.split('#')[0], claim: 'some-claim', + cnf: { + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + }, }) - - expect(sdJwtVcRecord.sdJwtVc.disclosures).toEqual(expect.arrayContaining([['salt', 'claim', 'some-claim']])) }) test('Create sd-jwt-vc from a basic payload with multiple (nested) disclosure', async () => { - const { compact, sdJwtVcRecord } = await sdJwtVcService.create( - agent.context, - { + const { compact, header, payload, prettyClaims } = await sdJwtVcService.sign(agent.context, { + disclosureFrame: { + is_over_65: true, + is_over_21: true, + is_over_18: true, + birthdate: true, + email: true, + address: { region: true, country: true }, + given_name: true, + }, + payload: { vct: 'IdentityCredential', given_name: 'John', family_name: 'Doe', @@ -171,30 +198,25 @@ describe('SdJwtVcService', () => { is_over_21: true, is_over_65: true, }, - { - issuerDidUrl: issuerDidUrl, - holderDidUrl: holderDidUrl, - disclosureFrame: { - is_over_65: true, - is_over_21: true, - is_over_18: true, - birthdate: true, - email: true, - address: { region: true, country: true }, - given_name: true, - }, - } - ) + holder: { + method: 'jwk', + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + }, + issuer: { + method: 'did', + didUrl: issuerDidUrl, + }, + }) expect(compact).toStrictEqual(complexSdJwtVc) - expect(sdJwtVcRecord.sdJwtVc.header).toEqual({ + expect(header).toEqual({ alg: 'EdDSA', typ: 'vc+sd-jwt', - kid: 'z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + kid: '#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', }) - expect(sdJwtVcRecord.sdJwtVc.payload).toEqual({ + expect(payload).toEqual({ vct: 'IdentityCredential', iat: Math.floor(new Date().getTime() / 1000), address: { @@ -215,117 +237,93 @@ describe('SdJwtVcService', () => { ], _sd_alg: 'sha-256', cnf: { - jwk: getJwkFromKey(holderKey).toJson(), + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), }, }) - expect(sdJwtVcRecord.sdJwtVc.payload).not.toContain({ - family_name: 'Doe', - phone_number: '+1-202-555-0101', + expect(prettyClaims).toEqual({ + vct: 'IdentityCredential', + iat: Math.floor(new Date().getTime() / 1000), address: { region: 'Anystate', country: 'US', + locality: 'Anytown', + street_address: '123 Main St', }, + email: 'johndoe@example.com', + given_name: 'John', + phone_number: '+1-202-555-0101', + family_name: 'Doe', + iss: issuerDidUrl.split('#')[0], birthdate: '1940-01-01', is_over_18: true, is_over_21: true, is_over_65: true, + cnf: { + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + }, }) - - expect(sdJwtVcRecord.sdJwtVc.disclosures).toEqual( - expect.arrayContaining([ - ['salt', 'is_over_65', true], - ['salt', 'is_over_21', true], - ['salt', 'is_over_18', true], - ['salt', 'birthdate', '1940-01-01'], - ['salt', 'email', 'johndoe@example.com'], - ['salt', 'region', 'Anystate'], - ['salt', 'country', 'US'], - ['salt', 'given_name', 'John'], - ]) - ) }) }) describe('SdJwtVcService.receive', () => { test('Receive sd-jwt-vc from a basic payload without disclosures', async () => { - const sdJwtVc = simpleJwtVc - - const sdJwtVcRecord = await sdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { - issuerDidUrl, - holderDidUrl, - }) - - await sdJwtVcService.storeCredential(agent.context, sdJwtVcRecord) + const sdJwtVc = await sdJwtVcService.fromCompact(simpleJwtVc) + const sdJwtVcRecord = await sdJwtVcService.store(agent.context, sdJwtVc.compact) + expect(sdJwtVcRecord.compactSdJwtVc).toEqual(simpleJwtVc) - expect(sdJwtVcRecord.sdJwtVc.header).toEqual({ + expect(sdJwtVc.header).toEqual({ alg: 'EdDSA', typ: 'vc+sd-jwt', - kid: 'z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + kid: '#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', }) - expect(sdJwtVcRecord.sdJwtVc.payload).toEqual({ + expect(sdJwtVc.payload).toEqual({ claim: 'some-claim', vct: 'IdentityCredential', iat: Math.floor(new Date().getTime() / 1000), iss: issuerDidUrl.split('#')[0], cnf: { - jwk: getJwkFromKey(holderKey).toJson(), + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), }, }) }) test('Receive sd-jwt-vc from a basic payload with a disclosure', async () => { - const sdJwtVc = sdJwtVcWithSingleDisclosure + const sdJwtVc = await sdJwtVcService.fromCompact(sdJwtVcWithSingleDisclosure) - const sdJwtVcRecord = await sdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { - issuerDidUrl, - holderDidUrl, - }) - - await sdJwtVcService.storeCredential(agent.context, sdJwtVcRecord) - - expect(sdJwtVcRecord.sdJwtVc.header).toEqual({ + expect(sdJwtVc.header).toEqual({ alg: 'EdDSA', typ: 'vc+sd-jwt', - kid: 'z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + kid: '#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', }) - expect(sdJwtVcRecord.sdJwtVc.payload).toEqual({ + expect(sdJwtVc.payload).toEqual({ vct: 'IdentityCredential', iat: Math.floor(new Date().getTime() / 1000), iss: issuerDidUrl.split('#')[0], _sd: ['vcvFU4DsFKTqQ1vl4nelJWXTb_-0dNoBks6iqNFptyg'], _sd_alg: 'sha-256', cnf: { - jwk: getJwkFromKey(holderKey).toJson(), + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), }, }) - expect(sdJwtVcRecord.sdJwtVc.payload).not.toContain({ + expect(sdJwtVc.payload).not.toContain({ claim: 'some-claim', }) - - expect(sdJwtVcRecord.sdJwtVc.disclosures).toEqual(expect.arrayContaining([['salt', 'claim', 'some-claim']])) }) test('Receive sd-jwt-vc from a basic payload with multiple (nested) disclosure', async () => { - const sdJwtVc = complexSdJwtVc + const sdJwtVc = await sdJwtVcService.fromCompact(complexSdJwtVc) - const sdJwtVcRecord = await sdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { - issuerDidUrl, - holderDidUrl, - }) - - await sdJwtVcService.storeCredential(agent.context, sdJwtVcRecord) - - expect(sdJwtVcRecord.sdJwtVc.header).toEqual({ + expect(sdJwtVc.header).toEqual({ alg: 'EdDSA', typ: 'vc+sd-jwt', - kid: 'z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + kid: '#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', }) - expect(sdJwtVcRecord.sdJwtVc.payload).toEqual({ + expect(sdJwtVc.payload).toEqual({ vct: 'IdentityCredential', iat: Math.floor(new Date().getTime() / 1000), family_name: 'Doe', @@ -335,6 +333,7 @@ describe('SdJwtVcService', () => { locality: 'Anytown', street_address: '123 Main St', }, + _sd_alg: 'sha-256', phone_number: '+1-202-555-0101', _sd: [ '1Cur2k2A2oIB5CshSIf_A_Kg-l26u_qKuWQ79P0Vdas', @@ -344,56 +343,59 @@ describe('SdJwtVcService', () => { 'psauKUNWEi09nu3Cl89xKXgmpWENZl5uy1N1nyn_jMk', 'sN_ge0pHXF6qmsYnX1A9SdwJ8ch8aENkxbODsT74YwI', ], - _sd_alg: 'sha-256', cnf: { - jwk: getJwkFromKey(holderKey).toJson(), + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), }, }) - expect(sdJwtVcRecord.sdJwtVc.payload).not.toContain({ - family_name: 'Doe', - phone_number: '+1-202-555-0101', + expect(sdJwtVc.payload).not.toContain({ address: { region: 'Anystate', country: 'US', }, + family_name: 'Doe', + phone_number: '+1-202-555-0101', + email: 'johndoe@example.com', + given_name: 'John', birthdate: '1940-01-01', is_over_18: true, is_over_21: true, is_over_65: true, }) - expect(sdJwtVcRecord.sdJwtVc.disclosures).toEqual( - expect.arrayContaining([ - ['salt', 'is_over_65', true], - ['salt', 'is_over_21', true], - ['salt', 'is_over_18', true], - ['salt', 'birthdate', '1940-01-01'], - ['salt', 'email', 'johndoe@example.com'], - ['salt', 'region', 'Anystate'], - ['salt', 'country', 'US'], - ['salt', 'given_name', 'John'], - ]) - ) + expect(sdJwtVc.prettyClaims).toEqual({ + vct: 'IdentityCredential', + iat: Math.floor(new Date().getTime() / 1000), + family_name: 'Doe', + iss: issuerDidUrl.split('#')[0], + phone_number: '+1-202-555-0101', + email: 'johndoe@example.com', + given_name: 'John', + birthdate: '1940-01-01', + is_over_18: true, + is_over_21: true, + is_over_65: true, + address: { + region: 'Anystate', + country: 'US', + locality: 'Anytown', + street_address: '123 Main St', + }, + cnf: { + jwk: jwkJsonWithoutUse(getJwkFromKey(holderKey)), + }, + }) }) }) describe('SdJwtVcService.present', () => { test('Present sd-jwt-vc from a basic payload without disclosures', async () => { - const sdJwtVc = simpleJwtVc - - const sdJwtVcRecord = await sdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { - issuerDidUrl, - holderDidUrl, - }) - - await sdJwtVcService.storeCredential(agent.context, sdJwtVcRecord) - - const presentation = await sdJwtVcService.present(agent.context, sdJwtVcRecord, { + const presentation = await sdJwtVcService.present(agent.context, { + compactSdJwtVc: simpleJwtVc, presentationFrame: {}, verifierMetadata: { issuedAt: new Date().getTime() / 1000, - verifierDid, + audience: verifierDid, nonce: await agent.context.wallet.generateNonce(), }, }) @@ -402,22 +404,14 @@ describe('SdJwtVcService', () => { }) test('Present sd-jwt-vc from a basic payload with a disclosure', async () => { - const sdJwtVc = sdJwtVcWithSingleDisclosure - - const sdJwtVcRecord = await sdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { - issuerDidUrl, - holderDidUrl, - }) - - await sdJwtVcService.storeCredential(agent.context, sdJwtVcRecord) - - const presentation = await sdJwtVcService.present(agent.context, sdJwtVcRecord, { + const presentation = await sdJwtVcService.present(agent.context, { + compactSdJwtVc: sdJwtVcWithSingleDisclosure, presentationFrame: { claim: true, }, verifierMetadata: { issuedAt: new Date().getTime() / 1000, - verifierDid, + audience: verifierDid, nonce: await agent.context.wallet.generateNonce(), }, }) @@ -426,9 +420,7 @@ describe('SdJwtVcService', () => { }) test('Present sd-jwt-vc from a basic payload with multiple (nested) disclosure', async () => { - const sdJwtVc = complexSdJwtVc - - const sdJwtVcRecord = await sdJwtVcService.fromSerializedJwt< + const presentation = await sdJwtVcService.present< Record, { // FIXME: when not passing a payload, adding nested presentationFrame is broken @@ -437,17 +429,11 @@ describe('SdJwtVcService', () => { country: string } } - >(agent.context, sdJwtVc, { - issuerDidUrl, - holderDidUrl, - }) - - await sdJwtVcService.storeCredential(agent.context, sdJwtVcRecord) - - const presentation = await sdJwtVcService.present(agent.context, sdJwtVcRecord, { + >(agent.context, { + compactSdJwtVc: complexSdJwtVc, verifierMetadata: { issuedAt: new Date().getTime() / 1000, - verifierDid, + audience: verifierDid, nonce: await agent.context.wallet.generateNonce(), }, presentationFrame: { @@ -467,34 +453,28 @@ describe('SdJwtVcService', () => { describe('SdJwtVcService.verify', () => { test('Verify sd-jwt-vc without disclosures', async () => { - const sdJwtVc = simpleJwtVc - - const sdJwtVcRecord = await sdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { - issuerDidUrl, - holderDidUrl, - }) - - await sdJwtVcService.storeCredential(agent.context, sdJwtVcRecord) - - const presentation = await sdJwtVcService.present(agent.context, sdJwtVcRecord, { + const nonce = await agent.context.wallet.generateNonce() + const presentation = await sdJwtVcService.present(agent.context, { + compactSdJwtVc: simpleJwtVc, // no disclosures presentationFrame: {}, verifierMetadata: { issuedAt: new Date().getTime() / 1000, - verifierDid, - nonce: await agent.context.wallet.generateNonce(), + audience: verifierDid, + nonce, }, }) - const { validation } = await sdJwtVcService.verify(agent.context, presentation, { - challenge: { verifierDid }, - holderDidUrl, + const { verification } = await sdJwtVcService.verify(agent.context, { + compactSdJwtVc: presentation, + keyBinding: { audience: verifierDid, nonce }, requiredClaimKeys: ['claim'], }) - expect(validation).toEqual({ + expect(verification).toEqual({ isSignatureValid: true, containsRequiredVcProperties: true, + containsExpectedKeyBinding: true, areRequiredClaimsIncluded: true, isValid: true, isKeyBindingValid: true, @@ -502,75 +482,63 @@ describe('SdJwtVcService', () => { }) test('Verify sd-jwt-vc with a disclosure', async () => { - const sdJwtVc = sdJwtVcWithSingleDisclosure - - const sdJwtVcRecord = await sdJwtVcService.fromSerializedJwt(agent.context, sdJwtVc, { - issuerDidUrl, - holderDidUrl, - }) + const nonce = await agent.context.wallet.generateNonce() - await sdJwtVcService.storeCredential(agent.context, sdJwtVcRecord) - - const presentation = await sdJwtVcService.present(agent.context, sdJwtVcRecord, { + const presentation = await sdJwtVcService.present(agent.context, { + compactSdJwtVc: sdJwtVcWithSingleDisclosure, verifierMetadata: { issuedAt: new Date().getTime() / 1000, - verifierDid, - nonce: await agent.context.wallet.generateNonce(), + audience: verifierDid, + nonce, }, presentationFrame: { claim: true, }, }) - const { validation } = await sdJwtVcService.verify(agent.context, presentation, { - challenge: { verifierDid }, - holderDidUrl, + const { verification } = await sdJwtVcService.verify(agent.context, { + compactSdJwtVc: presentation, + keyBinding: { audience: verifierDid, nonce }, requiredClaimKeys: ['vct', 'cnf', 'claim', 'iat'], }) - expect(validation).toEqual({ + expect(verification).toEqual({ isSignatureValid: true, containsRequiredVcProperties: true, areRequiredClaimsIncluded: true, isValid: true, isKeyBindingValid: true, + containsExpectedKeyBinding: true, }) }) test('Verify sd-jwt-vc with multiple (nested) disclosure', async () => { - const sdJwtVc = complexSdJwtVc + const nonce = await agent.context.wallet.generateNonce() - const sdJwtVcRecord = await sdJwtVcService.fromSerializedJwt( + const presentation = await sdJwtVcService.present( agent.context, - sdJwtVc, { - issuerDidUrl, - holderDidUrl, + compactSdJwtVc: complexSdJwtVc, + verifierMetadata: { + issuedAt: new Date().getTime() / 1000, + audience: verifierDid, + nonce, + }, + presentationFrame: { + is_over_65: true, + is_over_21: true, + email: true, + address: { + country: true, + }, + given_name: true, + }, } ) - await sdJwtVcService.storeCredential(agent.context, sdJwtVcRecord) - - const presentation = await sdJwtVcService.present(agent.context, sdJwtVcRecord, { - verifierMetadata: { - issuedAt: new Date().getTime() / 1000, - verifierDid, - nonce: await agent.context.wallet.generateNonce(), - }, - presentationFrame: { - is_over_65: true, - is_over_21: true, - email: true, - address: { - country: true, - }, - given_name: true, - }, - }) - - const { validation } = await sdJwtVcService.verify(agent.context, presentation, { - challenge: { verifierDid }, - holderDidUrl, + const { verification } = await sdJwtVcService.verify(agent.context, { + compactSdJwtVc: presentation, + keyBinding: { audience: verifierDid, nonce }, // FIXME: this should be a requiredFrame to be consistent with the other methods // using frames requiredClaimKeys: [ @@ -591,9 +559,10 @@ describe('SdJwtVcService', () => { ], }) - expect(validation).toEqual({ + expect(verification).toEqual({ isSignatureValid: true, areRequiredClaimsIncluded: true, + containsExpectedKeyBinding: true, containsRequiredVcProperties: true, isValid: true, isKeyBindingValid: true, diff --git a/packages/sd-jwt-vc/src/__tests__/sdjwtvc.fixtures.ts b/packages/sd-jwt-vc/src/__tests__/sdjwtvc.fixtures.ts index fa2c77e74c..6d3d76e483 100644 --- a/packages/sd-jwt-vc/src/__tests__/sdjwtvc.fixtures.ts +++ b/packages/sd-jwt-vc/src/__tests__/sdjwtvc.fixtures.ts @@ -1,17 +1,17 @@ export const simpleJwtVc = - 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Ino2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyJ9.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzJ9.dBWmF_E7uTHTw2t65mTJWldGZxu5_f-tOyQN7bljz1HLkqgkGuAbvx4aHlO12kTSRjs7rnwO0A79gz4CeE44Bg' + 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzJ9.hcNF6_PnQO4Gm0vqD_iblyBknUG0PeQLbIpPJ5s0P4UCQ7YdSSNCNL7VNOfzzAxZRWbH5knhje0_xYl6OXQ-CA' export const simpleJwtVcPresentation = - 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Ino2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyJ9.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzJ9.dBWmF_E7uTHTw2t65mTJWldGZxu5_f-tOyQN7bljz1HLkqgkGuAbvx4aHlO12kTSRjs7rnwO0A79gz4CeE44Bg~eyJhbGciOiJFZERTQSIsInR5cCI6ImtiK2p3dCJ9.eyJpYXQiOjE2OTgxNTE1MzIsIm5vbmNlIjoic2FsdCIsImF1ZCI6ImRpZDprZXk6elVDNzRWRXFxaEVIUWNndjR6YWdTUGtxRkp4dU5XdW9CUEtqSnVIRVRFVWVITG9TcVd0OTJ2aVNzbWFXank4MnkifQ.VdZSnQJ5sklqMPnIzaOaGxP2qPiEPniTaUFHy4VMcW9h9pV1c17fcuTySJtmV2tcpKhei4ss04q_rFyN1EVRDg' + 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzJ9.hcNF6_PnQO4Gm0vqD_iblyBknUG0PeQLbIpPJ5s0P4UCQ7YdSSNCNL7VNOfzzAxZRWbH5knhje0_xYl6OXQ-CA~eyJhbGciOiJFZERTQSIsInR5cCI6ImtiK2p3dCJ9.eyJpYXQiOjE2OTgxNTE1MzIsIm5vbmNlIjoic2FsdCIsImF1ZCI6ImRpZDprZXk6elVDNzRWRXFxaEVIUWNndjR6YWdTUGtxRkp4dU5XdW9CUEtqSnVIRVRFVWVITG9TcVd0OTJ2aVNzbWFXank4MnkiLCJfc2RfaGFzaCI6IkN4SnFuQ1Btd0d6bjg4YTlDdGhta2pHZXFXbnlKVTVKc2NLMXJ1VThOS28ifQ.0QaDyJrvZO91o7gdKPduKQIj5Z1gBAdWPNE8-PFqhj_rC56_I5aL8QtlwL8Mdl6iSjpUPDQ4LAN2JgB2nNOFBw' export const sdJwtVcWithSingleDisclosure = - 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Ino2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyJ9.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzIsIl9zZF9hbGciOiJzaGEtMjU2IiwiX3NkIjpbInZjdkZVNERzRktUcVExdmw0bmVsSldYVGJfLTBkTm9Ca3M2aXFORnB0eWciXX0.yUYqg_7fkgvh4vnoWW4L6OZpM1eAatAfKUUMhHt2xYdHtQYHdVOch1Om-mpN2lTsyw9L1sZ5KsuAx7-5T-jlDQ~WyJzYWx0IiwiY2xhaW0iLCJzb21lLWNsYWltIl0~' + 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzIsIl9zZF9hbGciOiJzaGEtMjU2IiwiX3NkIjpbInZjdkZVNERzRktUcVExdmw0bmVsSldYVGJfLTBkTm9Ca3M2aXFORnB0eWciXX0.Op3rwd7t6ZsdMSMa1EchAm31bP5aqLF6pB-Z1y-h3CFJYGmhNTkMpTeft1I3hSWq7QmbqBo1GKBEiZc7D9B9DA~WyJzYWx0IiwiY2xhaW0iLCJzb21lLWNsYWltIl0~' export const sdJwtVcWithSingleDisclosurePresentation = - 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Ino2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyJ9.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzIsIl9zZF9hbGciOiJzaGEtMjU2IiwiX3NkIjpbInZjdkZVNERzRktUcVExdmw0bmVsSldYVGJfLTBkTm9Ca3M2aXFORnB0eWciXX0.yUYqg_7fkgvh4vnoWW4L6OZpM1eAatAfKUUMhHt2xYdHtQYHdVOch1Om-mpN2lTsyw9L1sZ5KsuAx7-5T-jlDQ~WyJzYWx0IiwiY2xhaW0iLCJzb21lLWNsYWltIl0~eyJhbGciOiJFZERTQSIsInR5cCI6ImtiK2p3dCJ9.eyJpYXQiOjE2OTgxNTE1MzIsIm5vbmNlIjoic2FsdCIsImF1ZCI6ImRpZDprZXk6elVDNzRWRXFxaEVIUWNndjR6YWdTUGtxRkp4dU5XdW9CUEtqSnVIRVRFVWVITG9TcVd0OTJ2aVNzbWFXank4MnkifQ.VdZSnQJ5sklqMPnIzaOaGxP2qPiEPniTaUFHy4VMcW9h9pV1c17fcuTySJtmV2tcpKhei4ss04q_rFyN1EVRDg' + 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzIsIl9zZF9hbGciOiJzaGEtMjU2IiwiX3NkIjpbInZjdkZVNERzRktUcVExdmw0bmVsSldYVGJfLTBkTm9Ca3M2aXFORnB0eWciXX0.Op3rwd7t6ZsdMSMa1EchAm31bP5aqLF6pB-Z1y-h3CFJYGmhNTkMpTeft1I3hSWq7QmbqBo1GKBEiZc7D9B9DA~WyJzYWx0IiwiY2xhaW0iLCJzb21lLWNsYWltIl0~eyJhbGciOiJFZERTQSIsInR5cCI6ImtiK2p3dCJ9.eyJpYXQiOjE2OTgxNTE1MzIsIm5vbmNlIjoic2FsdCIsImF1ZCI6ImRpZDprZXk6elVDNzRWRXFxaEVIUWNndjR6YWdTUGtxRkp4dU5XdW9CUEtqSnVIRVRFVWVITG9TcVd0OTJ2aVNzbWFXank4MnkiLCJfc2RfaGFzaCI6IlBNbEo3bjhjdVdvUU9YTFZ4cTRhaWRaNHJTY2FrVUtMT1hUaUtWYjYtYTQifQ.5iYVLw6U7NIdW7Eoo2jYYBsR3fSJZ-ocOtI6rxl-GYUj8ZeCx_-IZ2rbwCMf71tq6M16x4ROooKGAdfWUSWQAg' export const complexSdJwtVc = - 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Ino2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyJ9.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJmYW1pbHlfbmFtZSI6IkRvZSIsInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsImFkZHJlc3MiOnsic3RyZWV0X2FkZHJlc3MiOiIxMjMgTWFpbiBTdCIsImxvY2FsaXR5IjoiQW55dG93biIsIl9zZCI6WyJOSm5tY3QwQnFCTUUxSmZCbEM2alJRVlJ1ZXZwRU9OaVl3N0E3TUh1SnlRIiwib201Wnp0WkhCLUdkMDBMRzIxQ1ZfeE00RmFFTlNvaWFPWG5UQUpOY3pCNCJdfSwiY25mIjp7Imp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Im9FTlZzeE9VaUg1NFg4d0pMYVZraWNDUmswMHdCSVE0c1JnYms1NE44TW8ifX0sImlzcyI6ImRpZDprZXk6ejZNa3RxdFhORzhDRFVZOVBycnRvU3RGemVDbmhwTW1neFlMMWdpa2NXM0J6dk5XIiwiaWF0IjoxNjk4MTUxNTMyLCJfc2RfYWxnIjoic2hhLTI1NiIsIl9zZCI6WyIxQ3VyMmsyQTJvSUI1Q3NoU0lmX0FfS2ctbDI2dV9xS3VXUTc5UDBWZGFzIiwiUjF6VFV2T1lIZ2NlcGowakh5cEdIejlFSHR0VktmdDB5c3diYzlFVFBiVSIsImVEcVFwZFRYSlhiV2hmLUVzSTd6dzVYNk92WW1GTi1VWlFRTWVzWHdLUHciLCJwZERrMl9YQUtIbzdnT0Fmd0YxYjdPZENVVlRpdDJrSkhheFNFQ1E5eGZjIiwicHNhdUtVTldFaTA5bnUzQ2w4OXhLWGdtcFdFTlpsNXV5MU4xbnluX2pNayIsInNOX2dlMHBIWEY2cW1zWW5YMUE5U2R3SjhjaDhhRU5reGJPRHNUNzRZd0kiXX0.Yz5U__nC0Nccza-NNfqhp-GueKXqeFNjm_NNtC1AJ2KdmERhCHdO6KNjM7bOiruHlo4oAlj-xObuB9LRiKXeCw~WyJzYWx0IiwiaXNfb3Zlcl82NSIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl8yMSIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl8xOCIsdHJ1ZV0~WyJzYWx0IiwiYmlydGhkYXRlIiwiMTk0MC0wMS0wMSJd~WyJzYWx0IiwiZW1haWwiLCJqb2huZG9lQGV4YW1wbGUuY29tIl0~WyJzYWx0IiwicmVnaW9uIiwiQW55c3RhdGUiXQ~WyJzYWx0IiwiY291bnRyeSIsIlVTIl0~WyJzYWx0IiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~' + 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJmYW1pbHlfbmFtZSI6IkRvZSIsInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsImFkZHJlc3MiOnsic3RyZWV0X2FkZHJlc3MiOiIxMjMgTWFpbiBTdCIsImxvY2FsaXR5IjoiQW55dG93biIsIl9zZCI6WyJOSm5tY3QwQnFCTUUxSmZCbEM2alJRVlJ1ZXZwRU9OaVl3N0E3TUh1SnlRIiwib201Wnp0WkhCLUdkMDBMRzIxQ1ZfeE00RmFFTlNvaWFPWG5UQUpOY3pCNCJdfSwiY25mIjp7Imp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Im9FTlZzeE9VaUg1NFg4d0pMYVZraWNDUmswMHdCSVE0c1JnYms1NE44TW8ifX0sImlzcyI6ImRpZDprZXk6ejZNa3RxdFhORzhDRFVZOVBycnRvU3RGemVDbmhwTW1neFlMMWdpa2NXM0J6dk5XIiwiaWF0IjoxNjk4MTUxNTMyLCJfc2RfYWxnIjoic2hhLTI1NiIsIl9zZCI6WyIxQ3VyMmsyQTJvSUI1Q3NoU0lmX0FfS2ctbDI2dV9xS3VXUTc5UDBWZGFzIiwiUjF6VFV2T1lIZ2NlcGowakh5cEdIejlFSHR0VktmdDB5c3diYzlFVFBiVSIsImVEcVFwZFRYSlhiV2hmLUVzSTd6dzVYNk92WW1GTi1VWlFRTWVzWHdLUHciLCJwZERrMl9YQUtIbzdnT0Fmd0YxYjdPZENVVlRpdDJrSkhheFNFQ1E5eGZjIiwicHNhdUtVTldFaTA5bnUzQ2w4OXhLWGdtcFdFTlpsNXV5MU4xbnluX2pNayIsInNOX2dlMHBIWEY2cW1zWW5YMUE5U2R3SjhjaDhhRU5reGJPRHNUNzRZd0kiXX0.coOK8NzJmEWz4qx-qRhjo-RK7aejrSkQM9La9Cw3eWmzcja9DXrkBoQZKbIJtNoSzSPLjwK2V71W78z0miZsDQ~WyJzYWx0IiwiaXNfb3Zlcl82NSIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl8yMSIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl8xOCIsdHJ1ZV0~WyJzYWx0IiwiYmlydGhkYXRlIiwiMTk0MC0wMS0wMSJd~WyJzYWx0IiwiZW1haWwiLCJqb2huZG9lQGV4YW1wbGUuY29tIl0~WyJzYWx0IiwicmVnaW9uIiwiQW55c3RhdGUiXQ~WyJzYWx0IiwiY291bnRyeSIsIlVTIl0~WyJzYWx0IiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~' export const complexSdJwtVcPresentation = - 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Ino2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyJ9.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJmYW1pbHlfbmFtZSI6IkRvZSIsInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsImFkZHJlc3MiOnsic3RyZWV0X2FkZHJlc3MiOiIxMjMgTWFpbiBTdCIsImxvY2FsaXR5IjoiQW55dG93biIsIl9zZCI6WyJOSm5tY3QwQnFCTUUxSmZCbEM2alJRVlJ1ZXZwRU9OaVl3N0E3TUh1SnlRIiwib201Wnp0WkhCLUdkMDBMRzIxQ1ZfeE00RmFFTlNvaWFPWG5UQUpOY3pCNCJdfSwiY25mIjp7Imp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Im9FTlZzeE9VaUg1NFg4d0pMYVZraWNDUmswMHdCSVE0c1JnYms1NE44TW8ifX0sImlzcyI6ImRpZDprZXk6ejZNa3RxdFhORzhDRFVZOVBycnRvU3RGemVDbmhwTW1neFlMMWdpa2NXM0J6dk5XIiwiaWF0IjoxNjk4MTUxNTMyLCJfc2RfYWxnIjoic2hhLTI1NiIsIl9zZCI6WyIxQ3VyMmsyQTJvSUI1Q3NoU0lmX0FfS2ctbDI2dV9xS3VXUTc5UDBWZGFzIiwiUjF6VFV2T1lIZ2NlcGowakh5cEdIejlFSHR0VktmdDB5c3diYzlFVFBiVSIsImVEcVFwZFRYSlhiV2hmLUVzSTd6dzVYNk92WW1GTi1VWlFRTWVzWHdLUHciLCJwZERrMl9YQUtIbzdnT0Fmd0YxYjdPZENVVlRpdDJrSkhheFNFQ1E5eGZjIiwicHNhdUtVTldFaTA5bnUzQ2w4OXhLWGdtcFdFTlpsNXV5MU4xbnluX2pNayIsInNOX2dlMHBIWEY2cW1zWW5YMUE5U2R3SjhjaDhhRU5reGJPRHNUNzRZd0kiXX0.Yz5U__nC0Nccza-NNfqhp-GueKXqeFNjm_NNtC1AJ2KdmERhCHdO6KNjM7bOiruHlo4oAlj-xObuB9LRiKXeCw~WyJzYWx0IiwiaXNfb3Zlcl82NSIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl8yMSIsdHJ1ZV0~WyJzYWx0IiwiZW1haWwiLCJqb2huZG9lQGV4YW1wbGUuY29tIl0~WyJzYWx0IiwiY291bnRyeSIsIlVTIl0~WyJzYWx0IiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~eyJhbGciOiJFZERTQSIsInR5cCI6ImtiK2p3dCJ9.eyJpYXQiOjE2OTgxNTE1MzIsIm5vbmNlIjoic2FsdCIsImF1ZCI6ImRpZDprZXk6elVDNzRWRXFxaEVIUWNndjR6YWdTUGtxRkp4dU5XdW9CUEtqSnVIRVRFVWVITG9TcVd0OTJ2aVNzbWFXank4MnkifQ.VdZSnQJ5sklqMPnIzaOaGxP2qPiEPniTaUFHy4VMcW9h9pV1c17fcuTySJtmV2tcpKhei4ss04q_rFyN1EVRDg' + 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJmYW1pbHlfbmFtZSI6IkRvZSIsInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsImFkZHJlc3MiOnsic3RyZWV0X2FkZHJlc3MiOiIxMjMgTWFpbiBTdCIsImxvY2FsaXR5IjoiQW55dG93biIsIl9zZCI6WyJOSm5tY3QwQnFCTUUxSmZCbEM2alJRVlJ1ZXZwRU9OaVl3N0E3TUh1SnlRIiwib201Wnp0WkhCLUdkMDBMRzIxQ1ZfeE00RmFFTlNvaWFPWG5UQUpOY3pCNCJdfSwiY25mIjp7Imp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Im9FTlZzeE9VaUg1NFg4d0pMYVZraWNDUmswMHdCSVE0c1JnYms1NE44TW8ifX0sImlzcyI6ImRpZDprZXk6ejZNa3RxdFhORzhDRFVZOVBycnRvU3RGemVDbmhwTW1neFlMMWdpa2NXM0J6dk5XIiwiaWF0IjoxNjk4MTUxNTMyLCJfc2RfYWxnIjoic2hhLTI1NiIsIl9zZCI6WyIxQ3VyMmsyQTJvSUI1Q3NoU0lmX0FfS2ctbDI2dV9xS3VXUTc5UDBWZGFzIiwiUjF6VFV2T1lIZ2NlcGowakh5cEdIejlFSHR0VktmdDB5c3diYzlFVFBiVSIsImVEcVFwZFRYSlhiV2hmLUVzSTd6dzVYNk92WW1GTi1VWlFRTWVzWHdLUHciLCJwZERrMl9YQUtIbzdnT0Fmd0YxYjdPZENVVlRpdDJrSkhheFNFQ1E5eGZjIiwicHNhdUtVTldFaTA5bnUzQ2w4OXhLWGdtcFdFTlpsNXV5MU4xbnluX2pNayIsInNOX2dlMHBIWEY2cW1zWW5YMUE5U2R3SjhjaDhhRU5reGJPRHNUNzRZd0kiXX0.coOK8NzJmEWz4qx-qRhjo-RK7aejrSkQM9La9Cw3eWmzcja9DXrkBoQZKbIJtNoSzSPLjwK2V71W78z0miZsDQ~WyJzYWx0IiwiaXNfb3Zlcl82NSIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl8yMSIsdHJ1ZV0~WyJzYWx0IiwiZW1haWwiLCJqb2huZG9lQGV4YW1wbGUuY29tIl0~WyJzYWx0IiwiY291bnRyeSIsIlVTIl0~WyJzYWx0IiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~eyJhbGciOiJFZERTQSIsInR5cCI6ImtiK2p3dCJ9.eyJpYXQiOjE2OTgxNTE1MzIsIm5vbmNlIjoic2FsdCIsImF1ZCI6ImRpZDprZXk6elVDNzRWRXFxaEVIUWNndjR6YWdTUGtxRkp4dU5XdW9CUEtqSnVIRVRFVWVITG9TcVd0OTJ2aVNzbWFXank4MnkiLCJfc2RfaGFzaCI6Ii1kTUd4OGZhUnpOQm91a2EwU0R6V2JkS3JYckw1TFVmUlNQTHN2Q2xPMFkifQ.TQQLqc4ZzoKjQfAghAzC_4aaU3KCS8YqzxAJtzT124guzkv9XSHtPN8d3z181_v-ca2ATXjTRoRciozitE6wBA' diff --git a/packages/sd-jwt-vc/src/index.ts b/packages/sd-jwt-vc/src/index.ts index daae9ef404..0d1891ea62 100644 --- a/packages/sd-jwt-vc/src/index.ts +++ b/packages/sd-jwt-vc/src/index.ts @@ -2,5 +2,5 @@ export * from './SdJwtVcApi' export * from './SdJwtVcModule' export * from './SdJwtVcService' export * from './SdJwtVcError' -export * from './SdJwtCredential' +export * from './SdJwtVcOptions' export * from './repository' diff --git a/packages/sd-jwt-vc/src/repository/SdJwtVcRecord.ts b/packages/sd-jwt-vc/src/repository/SdJwtVcRecord.ts index ca9e2bb7c7..50f43635ee 100644 --- a/packages/sd-jwt-vc/src/repository/SdJwtVcRecord.ts +++ b/packages/sd-jwt-vc/src/repository/SdJwtVcRecord.ts @@ -1,88 +1,46 @@ -import type { SdJwtVcHeader, SdJwtVcPayload } from '../SdJwtVcOptions' import type { TagsBase, Constructable } from '@aries-framework/core' -import type { DisclosureItem, HasherAndAlgorithm } from '@sd-jwt/core' +import type { Disclosure } from '@sd-jwt/core' -import { JsonTransformer, Hasher, TypedArrayEncoder, BaseRecord, utils } from '@aries-framework/core' -import { Disclosure, HasherAlgorithm, SdJwtVc } from '@sd-jwt/core' +import { JsonTransformer, BaseRecord, utils } from '@aries-framework/core' +import { SdJwtVc } from '@sd-jwt/core' export type DefaultSdJwtVcRecordTags = { disclosureKeys?: Array } -export type SdJwt
= { - disclosures?: Array - header: Header - payload: Payload - // FIXME: serializing Uint8Array to and from JSON does not work. Can we just store the SD-JWT in it's compact version? - signature: Uint8Array - - holderDidUrl: string -} - -export type SdJwtVcRecordStorageProps< - Header extends SdJwtVcHeader = SdJwtVcHeader, - Payload extends SdJwtVcPayload = SdJwtVcPayload -> = { +export type SdJwtVcRecordStorageProps = { id?: string createdAt?: Date tags?: TagsBase - sdJwtVc: SdJwt + compactSdJwtVc: string } -export class SdJwtVcRecord< - Header extends SdJwtVcHeader = SdJwtVcHeader, - Payload extends SdJwtVcPayload = SdJwtVcPayload -> extends BaseRecord { +export class SdJwtVcRecord extends BaseRecord { public static readonly type = 'SdJwtVcRecord' public readonly type = SdJwtVcRecord.type - public sdJwtVc!: SdJwt + // We store the sdJwtVc in compact format. + public compactSdJwtVc!: string + + // TODO: should we also store the pretty claims so it's not needed to + // re-calculate the hashes each time? I think for now it's fine to re-calculate - public constructor(props: SdJwtVcRecordStorageProps) { + public constructor(props: SdJwtVcRecordStorageProps) { super() if (props) { this.id = props.id ?? utils.uuid() this.createdAt = props.createdAt ?? new Date() - this.sdJwtVc = props.sdJwtVc + this.compactSdJwtVc = props.compactSdJwtVc this._tags = props.tags ?? {} } } - private get hasher(): HasherAndAlgorithm { - return { - algorithm: HasherAlgorithm.Sha256, - hasher: (input: string) => { - const serializedInput = TypedArrayEncoder.fromString(input) - return Hasher.hash(serializedInput, 'sha2-256') - }, - } - } - - /** - * This function gets the claims from the payload and combines them with the claims in the disclosures. - * - * This can be used to display all claims included in the `sd-jwt-vc` to the holder or verifier. - */ - public async getPrettyClaims | Payload = Payload>(): Promise { - const sdJwtVc = new SdJwtVc({ - header: this.sdJwtVc.header, - payload: this.sdJwtVc.payload, - disclosures: this.sdJwtVc.disclosures?.map(Disclosure.fromArray), - }).withHasher(this.hasher) - - // Assert that we only support `sha-256` as a hashing algorithm - if ('_sd_alg' in this.sdJwtVc.payload) { - sdJwtVc.assertClaimInPayload('_sd_alg', HasherAlgorithm.Sha256.toString()) - } - - return await sdJwtVc.getPrettyClaims() - } - public getTags() { - const disclosureKeys = this.sdJwtVc.disclosures - ?.filter((d): d is [string, string, unknown] => d.length === 3) - .map((d) => d[1]) + const disclosures = SdJwtVc.fromCompact(this.compactSdJwtVc).disclosures + const disclosureKeys = disclosures + ?.filter((d): d is Disclosure & { decoded: [string, string, unknown] } => d.decoded.length === 3) + .map((d) => d.decoded[1]) return { ...this._tags, diff --git a/packages/sd-jwt-vc/src/repository/__tests__/SdJwtVcRecord.test.ts b/packages/sd-jwt-vc/src/repository/__tests__/SdJwtVcRecord.test.ts index 90d47a18b9..622b1cca69 100644 --- a/packages/sd-jwt-vc/src/repository/__tests__/SdJwtVcRecord.test.ts +++ b/packages/sd-jwt-vc/src/repository/__tests__/SdJwtVcRecord.test.ts @@ -1,12 +1,11 @@ import { JsonTransformer } from '@aries-framework/core' -import { SdJwtVc, SignatureAndEncryptionAlgorithm } from '@sd-jwt/core' import { SdJwtVcRecord } from '../SdJwtVcRecord' describe('SdJwtVcRecord', () => { - const holderDidUrl = 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y' - test('sets the values passed in the constructor on the record', () => { + const compactSdJwtVc = + 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Ino2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyJ9.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJmYW1pbHlfbmFtZSI6IkRvZSIsInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsImFkZHJlc3MiOnsic3RyZWV0X2FkZHJlc3MiOiIxMjMgTWFpbiBTdCIsImxvY2FsaXR5IjoiQW55dG93biIsIl9zZCI6WyJOSm5tY3QwQnFCTUUxSmZCbEM2alJRVlJ1ZXZwRU9OaVl3N0E3TUh1SnlRIiwib201Wnp0WkhCLUdkMDBMRzIxQ1ZfeE00RmFFTlNvaWFPWG5UQUpOY3pCNCJdfSwiY25mIjp7Imp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Im9FTlZzeE9VaUg1NFg4d0pMYVZraWNDUmswMHdCSVE0c1JnYms1NE44TW8ifX0sImlzcyI6ImRpZDprZXk6ejZNa3RxdFhORzhDRFVZOVBycnRvU3RGemVDbmhwTW1neFlMMWdpa2NXM0J6dk5XIiwiaWF0IjoxNjk4MTUxNTMyLCJfc2RfYWxnIjoic2hhLTI1NiIsIl9zZCI6WyIxQ3VyMmsyQTJvSUI1Q3NoU0lmX0FfS2ctbDI2dV9xS3VXUTc5UDBWZGFzIiwiUjF6VFV2T1lIZ2NlcGowakh5cEdIejlFSHR0VktmdDB5c3diYzlFVFBiVSIsImVEcVFwZFRYSlhiV2hmLUVzSTd6dzVYNk92WW1GTi1VWlFRTWVzWHdLUHciLCJwZERrMl9YQUtIbzdnT0Fmd0YxYjdPZENVVlRpdDJrSkhheFNFQ1E5eGZjIiwicHNhdUtVTldFaTA5bnUzQ2w4OXhLWGdtcFdFTlpsNXV5MU4xbnluX2pNayIsInNOX2dlMHBIWEY2cW1zWW5YMUE5U2R3SjhjaDhhRU5reGJPRHNUNzRZd0kiXX0.Yz5U__nC0Nccza-NNfqhp-GueKXqeFNjm_NNtC1AJ2KdmERhCHdO6KNjM7bOiruHlo4oAlj-xObuB9LRiKXeCw~WyJzYWx0IiwiaXNfb3Zlcl82NSIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl8yMSIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl8xOCIsdHJ1ZV0~WyJzYWx0IiwiYmlydGhkYXRlIiwiMTk0MC0wMS0wMSJd~WyJzYWx0IiwiZW1haWwiLCJqb2huZG9lQGV4YW1wbGUuY29tIl0~WyJzYWx0IiwicmVnaW9uIiwiQW55c3RhdGUiXQ~WyJzYWx0IiwiY291bnRyeSIsIlVTIl0~WyJzYWx0IiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~' const createdAt = new Date() const sdJwtVcRecord = new SdJwtVcRecord({ id: 'sdjwt-id', @@ -14,12 +13,7 @@ describe('SdJwtVcRecord', () => { tags: { some: 'tag', }, - sdJwtVc: { - header: { alg: SignatureAndEncryptionAlgorithm.EdDSA }, - payload: { iss: 'did:key:123' }, - signature: new Uint8Array(32).fill(42), - holderDidUrl, - }, + compactSdJwtVc, }) expect(sdJwtVcRecord.type).toBe('SdJwtVcRecord') @@ -27,16 +21,23 @@ describe('SdJwtVcRecord', () => { expect(sdJwtVcRecord.createdAt).toBe(createdAt) expect(sdJwtVcRecord.getTags()).toEqual({ some: 'tag', + disclosureKeys: [ + 'is_over_65', + 'is_over_21', + 'is_over_18', + 'birthdate', + 'email', + 'region', + 'country', + 'given_name', + ], }) - expect(sdJwtVcRecord.sdJwtVc).toEqual({ - header: { alg: SignatureAndEncryptionAlgorithm.EdDSA }, - payload: { iss: 'did:key:123' }, - signature: new Uint8Array(32).fill(42), - holderDidUrl, - }) + expect(sdJwtVcRecord.compactSdJwtVc).toEqual(compactSdJwtVc) }) test('serializes and deserializes', () => { + const compactSdJwtVc = + 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Ino2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyJ9.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJmYW1pbHlfbmFtZSI6IkRvZSIsInBob25lX251bWJlciI6IisxLTIwMi01NTUtMDEwMSIsImFkZHJlc3MiOnsic3RyZWV0X2FkZHJlc3MiOiIxMjMgTWFpbiBTdCIsImxvY2FsaXR5IjoiQW55dG93biIsIl9zZCI6WyJOSm5tY3QwQnFCTUUxSmZCbEM2alJRVlJ1ZXZwRU9OaVl3N0E3TUh1SnlRIiwib201Wnp0WkhCLUdkMDBMRzIxQ1ZfeE00RmFFTlNvaWFPWG5UQUpOY3pCNCJdfSwiY25mIjp7Imp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Im9FTlZzeE9VaUg1NFg4d0pMYVZraWNDUmswMHdCSVE0c1JnYms1NE44TW8ifX0sImlzcyI6ImRpZDprZXk6ejZNa3RxdFhORzhDRFVZOVBycnRvU3RGemVDbmhwTW1neFlMMWdpa2NXM0J6dk5XIiwiaWF0IjoxNjk4MTUxNTMyLCJfc2RfYWxnIjoic2hhLTI1NiIsIl9zZCI6WyIxQ3VyMmsyQTJvSUI1Q3NoU0lmX0FfS2ctbDI2dV9xS3VXUTc5UDBWZGFzIiwiUjF6VFV2T1lIZ2NlcGowakh5cEdIejlFSHR0VktmdDB5c3diYzlFVFBiVSIsImVEcVFwZFRYSlhiV2hmLUVzSTd6dzVYNk92WW1GTi1VWlFRTWVzWHdLUHciLCJwZERrMl9YQUtIbzdnT0Fmd0YxYjdPZENVVlRpdDJrSkhheFNFQ1E5eGZjIiwicHNhdUtVTldFaTA5bnUzQ2w4OXhLWGdtcFdFTlpsNXV5MU4xbnluX2pNayIsInNOX2dlMHBIWEY2cW1zWW5YMUE5U2R3SjhjaDhhRU5reGJPRHNUNzRZd0kiXX0.Yz5U__nC0Nccza-NNfqhp-GueKXqeFNjm_NNtC1AJ2KdmERhCHdO6KNjM7bOiruHlo4oAlj-xObuB9LRiKXeCw~WyJzYWx0IiwiaXNfb3Zlcl82NSIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl8yMSIsdHJ1ZV0~WyJzYWx0IiwiaXNfb3Zlcl8xOCIsdHJ1ZV0~WyJzYWx0IiwiYmlydGhkYXRlIiwiMTk0MC0wMS0wMSJd~WyJzYWx0IiwiZW1haWwiLCJqb2huZG9lQGV4YW1wbGUuY29tIl0~WyJzYWx0IiwicmVnaW9uIiwiQW55c3RhdGUiXQ~WyJzYWx0IiwiY291bnRyeSIsIlVTIl0~WyJzYWx0IiwiZ2l2ZW5fbmFtZSIsIkpvaG4iXQ~' const createdAt = new Date('2022-02-02') const sdJwtVcRecord = new SdJwtVcRecord({ id: 'sdjwt-id', @@ -44,12 +45,7 @@ describe('SdJwtVcRecord', () => { tags: { some: 'tag', }, - sdJwtVc: { - header: { alg: SignatureAndEncryptionAlgorithm.EdDSA }, - payload: { iss: 'did:key:123' }, - signature: new Uint8Array(32).fill(42), - holderDidUrl, - }, + compactSdJwtVc, }) const json = sdJwtVcRecord.toJSON() @@ -60,11 +56,7 @@ describe('SdJwtVcRecord', () => { _tags: { some: 'tag', }, - sdJwtVc: { - header: { alg: SignatureAndEncryptionAlgorithm.EdDSA }, - payload: { iss: 'did:key:123' }, - signature: new Uint8Array(32).fill(42), - }, + compactSdJwtVc, }) const instance = JsonTransformer.deserialize(JSON.stringify(json), SdJwtVcRecord) @@ -74,49 +66,17 @@ describe('SdJwtVcRecord', () => { expect(instance.createdAt.getTime()).toBe(createdAt.getTime()) expect(instance.getTags()).toEqual({ some: 'tag', + disclosureKeys: [ + 'is_over_65', + 'is_over_21', + 'is_over_18', + 'birthdate', + 'email', + 'region', + 'country', + 'given_name', + ], }) - expect(instance.sdJwtVc.signature).toBeInstanceOf(Uint8Array) - expect(instance.sdJwtVc).toMatchObject({ - header: { alg: SignatureAndEncryptionAlgorithm.EdDSA }, - payload: { iss: 'did:key:123' }, - signature: new Uint8Array(32).fill(42), - }) - }) - - test('Get the pretty claims', async () => { - const compactSdJwtVc = - 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6Ino2TWt0cXRYTkc4Q0RVWTlQcnJ0b1N0RnplQ25ocE1tZ3hZTDFnaWtjVzNCenZOVyJ9.eyJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzIsIl9zZF9hbGciOiJzaGEtMjU2IiwiX3NkIjpbInZjdkZVNERzRktUcVExdmw0bmVsSldYVGJfLTBkTm9Ca3M2aXFORnB0eWciXX0.yUYqg_7fkgvh4vnoWW4L6OZpM1eAatAfKUUMhHt2xYdHtQYHdVOch1Om-mpN2lTsyw9L1sZ5KsuAx7-5T-jlDQ~WyJzYWx0IiwiY2xhaW0iLCJzb21lLWNsYWltIl0~' - - const sdJwtVc = SdJwtVc.fromCompact(compactSdJwtVc) - - const sdJwtVcRecord = new SdJwtVcRecord({ - tags: { - some: 'tag', - }, - sdJwtVc: { - header: sdJwtVc.header, - payload: sdJwtVc.payload, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - signature: sdJwtVc.signature!, - disclosures: sdJwtVc.disclosures?.map((d) => d.decoded), - holderDidUrl, - }, - }) - - const prettyClaims = await sdJwtVcRecord.getPrettyClaims() - - expect(prettyClaims).toEqual({ - vct: 'IdentityCredential', - cnf: { - jwk: { - kty: 'OKP', - crv: 'Ed25519', - x: 'oENVsxOUiH54X8wJLaVkicCRk00wBIQ4sRgbk54N8Mo', - }, - }, - iss: 'did:key:z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', - iat: 1698151532, - claim: 'some-claim', - }) + expect(instance.compactSdJwtVc).toBe(compactSdJwtVc) }) }) diff --git a/packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts b/packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts index 95ccc5f633..d45afb35a6 100644 --- a/packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts +++ b/packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts @@ -2,6 +2,7 @@ import type { Key } from '@aries-framework/core' import { AskarModule } from '@aries-framework/askar' import { + getJwkFromKey, Agent, DidKey, DidsModule, @@ -37,7 +38,6 @@ describe('sd-jwt-vc end to end test', () => { const holder = getAgent('sdjwtvcholderagent') let holderKey: Key - let holderDidUrl: string const verifier = getAgent('sdjwtvcverifieragent') const verifierDid = 'did:key:zUC74VEqqhEHQcgv4zagSPkqFJxuNWuoBPKjJuHETEUeHLoSqWt92viSsmaWjy82y' @@ -60,11 +60,6 @@ describe('sd-jwt-vc end to end test', () => { seed: TypedArrayEncoder.fromString('00000000000000000000000000000001'), }) - const holderDidKey = new DidKey(holderKey) - const holderDidDocument = holderDidKey.didDocument - holderDidUrl = (holderDidDocument.verificationMethod ?? [])[0].id - await holder.dids.import({ didDocument: holderDidDocument, did: holderDidDocument.id }) - await verifier.initialize() }) @@ -87,9 +82,23 @@ describe('sd-jwt-vc end to end test', () => { is_over_65: true, } as const - const { compact, sdJwtVcRecord: _sdJwtVcRecord } = await issuer.modules.sdJwt.create(credential, { - holderDidUrl, - issuerDidUrl, + const { compact, header, payload } = await issuer.modules.sdJwt.sign({ + payload: credential, + // FIXME: sd-jwt library does not support did binding for holder yet + // issuance is fine, but in verification of KB the jwk will be extracted + // from the cnf claim which will be undefined. + // holder: { + // method: 'did', + // didUrl: holderDidUrl, + // }, + holder: { + method: 'jwk', + jwk: getJwkFromKey(holderKey), + }, + issuer: { + didUrl: issuerDidUrl, + method: 'did', + }, disclosureFrame: { is_over_65: true, is_over_21: true, @@ -104,24 +113,99 @@ describe('sd-jwt-vc end to end test', () => { }, }) - type Payload = (typeof _sdJwtVcRecord)['sdJwtVc']['payload'] - type Header = (typeof _sdJwtVcRecord)['sdJwtVc']['header'] + type Payload = typeof payload + type Header = typeof header + + // parse SD-JWT + const sdJwtVc = await holder.modules.sdJwt.fromCompact(compact) + expect(sdJwtVc).toEqual({ + compact: expect.any(String), + header: { + alg: 'EdDSA', + kid: '#z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + typ: 'vc+sd-jwt', + }, + payload: { + _sd: [ + expect.any(String), + expect.any(String), + expect.any(String), + expect.any(String), + expect.any(String), + expect.any(String), + expect.any(String), + expect.any(String), + expect.any(String), + expect.any(String), + ], + _sd_alg: 'sha-256', + address: { + _sd: [ + expect.any(String), + expect.any(String), + expect.any(String), + expect.any(String), + expect.any(String), + expect.any(String), + ], + }, + cnf: { + jwk: { + crv: 'Ed25519', + kty: 'OKP', + x: 'oENVsxOUiH54X8wJLaVkicCRk00wBIQ4sRgbk54N8Mo', + }, + }, + iat: expect.any(Number), + iss: 'did:key:z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + vct: 'IdentityCredential', + }, + prettyClaims: { + address: { + country: 'US', + locality: 'Anytown', + region: 'Anystate', + street_address: '123 Main St', + }, + birthdate: '1940-01-01', + cnf: { + jwk: { + crv: 'Ed25519', + kty: 'OKP', + x: 'oENVsxOUiH54X8wJLaVkicCRk00wBIQ4sRgbk54N8Mo', + }, + }, + email: 'johndoe@example.com', + family_name: 'Doe', + given_name: 'John', + iat: expect.any(Number), + is_over_18: true, + is_over_21: true, + is_over_65: true, + iss: 'did:key:z6MktqtXNG8CDUY9PrrtoStFzeCnhpMmgxYL1gikcW3BzvNW', + phone_number: '+1-202-555-0101', + vct: 'IdentityCredential', + }, + }) - const sdJwtVcRecord = await holder.modules.sdJwt.fromSerializedJwt(compact, { - issuerDidUrl, - holderDidUrl, + // Verify SD-JWT (does not require key binding) + const { verification } = await holder.modules.sdJwt.verify({ + compactSdJwtVc: compact, }) + expect(verification.isValid).toBe(true) - await holder.modules.sdJwt.storeCredential(sdJwtVcRecord) + // Store credential + await holder.modules.sdJwt.store(compact) // Metadata created by the verifier and send out of band by the verifier to the holder const verifierMetadata = { - verifierDid, + audience: verifierDid, issuedAt: new Date().getTime() / 1000, nonce: await verifier.wallet.generateNonce(), } - const presentation = await holder.modules.sdJwt.present(sdJwtVcRecord, { + const presentation = await holder.modules.sdJwt.present({ + compactSdJwtVc: compact, verifierMetadata, presentationFrame: { vct: true, @@ -142,11 +226,9 @@ describe('sd-jwt-vc end to end test', () => { }, }) - const { - validation: { isValid }, - } = await verifier.modules.sdJwt.verify(presentation, { - holderDidUrl, - challenge: { verifierDid }, + const { verification: presentationVerification } = await verifier.modules.sdJwt.verify({ + compactSdJwtVc: presentation, + keyBinding: { audience: verifierDid, nonce: verifierMetadata.nonce }, requiredClaimKeys: [ 'is_over_65', 'is_over_21', @@ -163,6 +245,6 @@ describe('sd-jwt-vc end to end test', () => { ], }) - expect(isValid).toBeTruthy() + expect(presentationVerification.isValid).toBeTruthy() }) }) From d9896d70074b3d24ddd869d4e2e0ed46409b365f Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Wed, 10 Jan 2024 16:36:18 +0700 Subject: [PATCH 092/115] typing improvements Signed-off-by: Timo Glastra --- packages/core/src/crypto/jose/jwt/Jwt.ts | 2 ++ packages/openid4vc/src/index.ts | 1 + packages/openid4vc/src/shared/index.ts | 1 + packages/openid4vc/src/shared/models/index.ts | 10 ++++++++++ packages/openid4vc/tests/utilsVci.ts | 20 ++++++++----------- 5 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 packages/openid4vc/src/shared/index.ts create mode 100644 packages/openid4vc/src/shared/models/index.ts diff --git a/packages/core/src/crypto/jose/jwt/Jwt.ts b/packages/core/src/crypto/jose/jwt/Jwt.ts index 33bb5c9178..cca3c3a454 100644 --- a/packages/core/src/crypto/jose/jwt/Jwt.ts +++ b/packages/core/src/crypto/jose/jwt/Jwt.ts @@ -1,4 +1,5 @@ import type { Buffer } from '../../../utils' +import type { JwkJson } from '../jwk' import { AriesFrameworkError } from '../../../error' import { JsonEncoder, TypedArrayEncoder } from '../../../utils' @@ -9,6 +10,7 @@ import { JwtPayload } from './JwtPayload' interface JwtHeader { alg: string kid?: string + jwk: JwkJson [key: string]: unknown } diff --git a/packages/openid4vc/src/index.ts b/packages/openid4vc/src/index.ts index 11b105d20d..222f8329c6 100644 --- a/packages/openid4vc/src/index.ts +++ b/packages/openid4vc/src/index.ts @@ -1,3 +1,4 @@ export * from './openid4vc-holder' export * from './openid4vc-verifier' export * from './openid4vc-issuer' +export * from './shared' diff --git a/packages/openid4vc/src/shared/index.ts b/packages/openid4vc/src/shared/index.ts new file mode 100644 index 0000000000..ad200c539f --- /dev/null +++ b/packages/openid4vc/src/shared/index.ts @@ -0,0 +1 @@ +export * from './models' diff --git a/packages/openid4vc/src/shared/models/index.ts b/packages/openid4vc/src/shared/models/index.ts new file mode 100644 index 0000000000..b3223a1aea --- /dev/null +++ b/packages/openid4vc/src/shared/models/index.ts @@ -0,0 +1,10 @@ +import type { + AssertedUniformCredentialOffer, + CredentialSupported, + UniformCredentialRequest, +} from '@sphereon/oid4vci-common' + +export type OpenId4VciCredentialSupportedWithId = CredentialSupported & { id: string } +export type OpenId4VciCredentialSupported = CredentialSupported +export type OpenId4VciCredentialRequest = UniformCredentialRequest +export type OpenId4VciCredentialOffer = AssertedUniformCredentialOffer diff --git a/packages/openid4vc/tests/utilsVci.ts b/packages/openid4vc/tests/utilsVci.ts index f17937f5a5..3652b4d42c 100644 --- a/packages/openid4vc/tests/utilsVci.ts +++ b/packages/openid4vc/tests/utilsVci.ts @@ -1,20 +1,20 @@ -import type { CredentialSupported } from '@sphereon/oid4vci-common' +import type { OpenId4VciCredentialSupportedWithId } from '../src' import { OpenIdCredentialFormatProfile } from '../src' -export const openBadgeCredential: CredentialSupported & { id: string } = { +export const openBadgeCredential: OpenId4VciCredentialSupportedWithId = { id: `/credentials/OpenBadgeCredential`, format: OpenIdCredentialFormatProfile.JwtVcJson, types: ['VerifiableCredential', 'OpenBadgeCredential'], } -export const universityDegreeCredential: CredentialSupported & { id: string } = { +export const universityDegreeCredential: OpenId4VciCredentialSupportedWithId = { id: `/credentials/UniversityDegreeCredential`, format: OpenIdCredentialFormatProfile.JwtVcJson, types: ['VerifiableCredential', 'UniversityDegreeCredential'], } -export const universityDegreeCredentialLd: CredentialSupported & { id: string } = { +export const universityDegreeCredentialLd: OpenId4VciCredentialSupportedWithId = { id: `/credentials/UniversityDegreeCredentialLd`, format: OpenIdCredentialFormatProfile.JwtVcJsonLd, types: ['VerifiableCredential', 'UniversityDegreeCredential'], @@ -24,18 +24,14 @@ export const universityDegreeCredentialLd: CredentialSupported & { id: string } export const universityDegreeCredentialSdJwt = { id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredentialSdJwt', format: OpenIdCredentialFormatProfile.SdJwtVc, - credential_definition: { - vct: 'UniversityDegreeCredential', - }, -} satisfies CredentialSupported & { id: string } + vct: 'UniversityDegreeCredential', +} satisfies OpenId4VciCredentialSupportedWithId export const universityDegreeCredentialSdJwt2 = { id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredentialSdJwt2', format: OpenIdCredentialFormatProfile.SdJwtVc, - credential_definition: { - vct: 'UniversityDegreeCredential2', - }, -} satisfies CredentialSupported & { id: string } + vct: 'UniversityDegreeCredential2', +} satisfies OpenId4VciCredentialSupportedWithId export const allCredentialsSupported = [ openBadgeCredential, From 17a2d5cfe63f6e9e37ff9bbbf17b6a79f2f5645b Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Wed, 10 Jan 2024 16:36:50 +0700 Subject: [PATCH 093/115] oid4vc issuer record Signed-off-by: Timo Glastra --- .../repository/OpenId4VcIssuerRecord.ts | 61 +++++++++++++++++++ .../repository/OpenId4VcIssuerRepository.ts | 23 +++++++ .../src/openid4vc-issuer/repository/index.ts | 2 + 3 files changed, 86 insertions(+) create mode 100644 packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRecord.ts create mode 100644 packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRepository.ts create mode 100644 packages/openid4vc/src/openid4vc-issuer/repository/index.ts diff --git a/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRecord.ts b/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRecord.ts new file mode 100644 index 0000000000..30935554fd --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRecord.ts @@ -0,0 +1,61 @@ +import type { OpenId4VciCredentialSupportedWithId } from '../../shared' +import type { RecordTags, TagsBase } from '@aries-framework/core' +import type { CredentialSupported, MetadataDisplay } from '@sphereon/oid4vci-common' + +import { BaseRecord, utils } from '@aries-framework/core' + +export type OpenId4VcIssuerRecordTags = RecordTags + +export type DefaultOpenId4VcIssuerRecordTags = { + issuerId: string +} + +export interface OpenId4VcIssuerRecordProps { + id?: string + createdAt?: Date + tags?: TagsBase + + issuerId: string + + /** + * The fingerprint (multibase encoded) of the public key used to sign access tokens for + * this issuer. + */ + accessTokenPublicKeyFingerprint: string + + credentialsSupported: OpenId4VciCredentialSupportedWithId[] + display?: MetadataDisplay[] +} + +export class OpenId4VcIssuerRecord extends BaseRecord { + public static readonly type = 'OpenId4VcIssuerRecord' + public readonly type = OpenId4VcIssuerRecord.type + + public issuerId!: string + public accessTokenPublicKeyFingerprint!: string + + public credentialsSupported!: CredentialSupported[] + public display?: MetadataDisplay[] + + public constructor(props: OpenId4VcIssuerRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.createdAt = props.createdAt ?? new Date() + this._tags = props.tags ?? {} + + this.issuerId = props.issuerId + this.accessTokenPublicKeyFingerprint = props.accessTokenPublicKeyFingerprint + this.credentialsSupported = props.credentialsSupported + this.display = props.display + } + } + + public getTags() { + return { + ...this._tags, + issuerId: this.issuerId, + } + } +} diff --git a/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRepository.ts b/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRepository.ts new file mode 100644 index 0000000000..f6387ef514 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRepository.ts @@ -0,0 +1,23 @@ +import type { AgentContext } from '@aries-framework/core' + +import { Repository, StorageService, InjectionSymbols, EventEmitter, inject, injectable } from '@aries-framework/core' + +import { OpenId4VcIssuerRecord } from './OpenId4VcIssuerRecord' + +@injectable() +export class OpenId4VcIssuerRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(OpenId4VcIssuerRecord, storageService, eventEmitter) + } + + public findByIssuerId(agentContext: AgentContext, issuerId: string) { + return this.findSingleByQuery(agentContext, { issuerId }) + } + + public getByIssuerId(agentContext: AgentContext, issuerId: string) { + return this.getSingleByQuery(agentContext, { issuerId }) + } +} diff --git a/packages/openid4vc/src/openid4vc-issuer/repository/index.ts b/packages/openid4vc/src/openid4vc-issuer/repository/index.ts new file mode 100644 index 0000000000..8b124ec167 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/repository/index.ts @@ -0,0 +1,2 @@ +export * from './OpenId4VcIssuerRecord' +export * from './OpenId4VcIssuerRepository' From f140305e616243ab79d303a613077d49330e2b28 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Wed, 10 Jan 2024 22:27:12 +0700 Subject: [PATCH 094/115] revamp issuer api Signed-off-by: Timo Glastra --- .../openid4vc-issuer/OpenId4VcIssuerApi.ts | 108 ++-- .../openid4vc-issuer/OpenId4VcIssuerModule.ts | 79 ++- .../OpenId4VcIssuerModuleConfig.ts | 107 +-- .../OpenId4VcIssuerService.ts | 611 +++++++++--------- .../OpenId4VcIssuerServiceOptions.ts | 174 +++-- .../openid4vc/src/openid4vc-issuer/index.ts | 1 + .../router/OpenId4VcIEndpointConfiguration.ts | 114 ---- .../router/accessTokenEndpoint.ts | 99 +-- .../router/credentialEndpoint.ts | 27 + .../src/openid4vc-issuer/router/express.ts | 12 + .../src/openid4vc-issuer/router/index.ts | 4 + .../router/metadataEndpoint.ts | 34 +- .../openid4vc-issuer/router/requestContext.ts | 61 ++ packages/openid4vc/src/shared/router.ts | 46 +- packages/sd-jwt-vc/src/SdJwtVcOptions.ts | 2 +- 15 files changed, 816 insertions(+), 663 deletions(-) delete mode 100644 packages/openid4vc/src/openid4vc-issuer/router/OpenId4VcIEndpointConfiguration.ts create mode 100644 packages/openid4vc/src/openid4vc-issuer/router/credentialEndpoint.ts create mode 100644 packages/openid4vc/src/openid4vc-issuer/router/express.ts create mode 100644 packages/openid4vc/src/openid4vc-issuer/router/index.ts create mode 100644 packages/openid4vc/src/openid4vc-issuer/router/requestContext.ts diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts index 045462cff9..e4b4038e36 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts @@ -1,15 +1,14 @@ import type { - CreateIssueCredentialResponseOptions, - CreateCredentialOfferAndRequestOptions, - CredentialOfferAndRequest, - OfferedCredential, - IssuerEndpointConfig, + CreateCredentialResponseOptions, + CreateCredentialOfferOptions, + CredentialOffer, } from './OpenId4VcIssuerServiceOptions' +import type { OpenId4VcIssuerRecordProps } from './repository/OpenId4VcIssuerRecord' import type { CredentialOfferPayloadV1_0_11 } from '@sphereon/oid4vci-common' -import type { Router } from 'express' import { injectable, AgentContext } from '@aries-framework/core' +import { OpenId4VcIssuerModuleConfig } from './OpenId4VcIssuerModuleConfig' import { OpenId4VcIssuerService } from './OpenId4VcIssuerService' /** @@ -20,43 +19,81 @@ import { OpenId4VcIssuerService } from './OpenId4VcIssuerService' */ @injectable() export class OpenId4VcIssuerApi { + /** + * Configuration for the credentials module + */ + public readonly config: OpenId4VcIssuerModuleConfig + private agentContext: AgentContext private openId4VcIssuerService: OpenId4VcIssuerService - public constructor(agentContext: AgentContext, openId4VcIssuerService: OpenId4VcIssuerService) { + public constructor( + agentContext: AgentContext, + openId4VcIssuerService: OpenId4VcIssuerService, + config: OpenId4VcIssuerModuleConfig + ) { this.agentContext = agentContext this.openId4VcIssuerService = openId4VcIssuerService + this.config = config + } + + public async getAllIssuers() { + return this.openId4VcIssuerService.getAllIssuers(this.agentContext) + } + + public async getByIssuerId(issuerId: string) { + return this.openId4VcIssuerService.getByIssuerId(this.agentContext, issuerId) } /** - * Creates a credential offer, and credential offer request. - * Either the preAuthorizedCodeFlowConfig or the authorizationCodeFlowConfig must be provided. - * - * @param offeredCredentials - The credentials to be offered. - * @param options.issuerMetadata - Metadata about the issuer. - * @param options.credentialOfferUri - The URI which references the created credential offer if the offer is passed by reference. - * @param options.preAuthorizedCodeFlowConfig - The configuration for the pre-authorized code flow. This or the authorizationCodeFlowConfig must be provided. - * @param options.authorizationCodeFlowConfig - The configuration for the authorization code flow. This or the preAuthorizedCodeFlowConfig must be provided. - * @param options.scheme - The credential offer request scheme. Default is 'https'. - * @param options.baseUri - The base URI of the credential offer request. Default is ''. + * Creates an issuer and stores the corresponding issuer metadata. Multiple issuers can be created, to allow different sets of + * credentials to be issued with each issuer. + */ + public async createIssuer(options: Pick) { + return this.openId4VcIssuerService.createIssuer(this.agentContext, options) + } + + /** + * Rotate the key used for signing access tokens for the issuer with the given issuerId. + */ + public async rotateAccessTokenSigningKey(issuerId: string) { + const issuer = await this.openId4VcIssuerService.getByIssuerId(this.agentContext, issuerId) + return this.openId4VcIssuerService.rotateAccessTokenSigningKey(this.agentContext, issuer) + } + + public async getIssuerMetadata(issuerId: string) { + const issuer = await this.openId4VcIssuerService.getByIssuerId(this.agentContext, issuerId) + return this.openId4VcIssuerService.getIssuerMetadata(this.agentContext, issuer) + } + + public async updateIssuerMetadata( + options: Pick + ) { + const issuer = await this.openId4VcIssuerService.getByIssuerId(this.agentContext, options.issuerId) + + issuer.credentialsSupported = options.credentialsSupported + issuer.display = options.display + + return this.openId4VcIssuerService.updateIssuer(this.agentContext, issuer) + } + + /** + * Creates a credential offer. Either the preAuthorizedCodeFlowConfig or the authorizationCodeFlowConfig must be provided. * * @returns Object containing the payload of the credential offer and the credential offer request, which can be sent to the wallet. */ - public async createCredentialOfferAndRequest( - offeredCredentials: OfferedCredential[], - options: CreateCredentialOfferAndRequestOptions - ): Promise { - return await this.openId4VcIssuerService.createCredentialOfferAndRequest( - this.agentContext, - offeredCredentials, - options - ) + public async createCredentialOffer( + options: CreateCredentialOfferOptions & { issuerId: string } + ): Promise { + const { issuerId, ...rest } = options + const issuer = await this.openId4VcIssuerService.getByIssuerId(this.agentContext, issuerId) + return await this.openId4VcIssuerService.createCredentialOffer(this.agentContext, { ...rest, issuer }) } /** * This function retrieves the credential offer referenced by the given URI. * Retrieving a credential offer from a URI is possible after a credential offer was created with - * @see createCredentialOfferAndRequest and the credentialOfferUri option. + * @see createCredentialOffer and the credentialOfferUri option. * * @throws if no credential offer can found for the given URI. * @param uri - The URI referencing the credential offer. @@ -73,18 +110,9 @@ export class OpenId4VcIssuerApi { * @param options.credential - The credential to be issued. * @param options.verificationMethod - The verification method used for signing the credential. */ - public async createIssueCredentialResponse(options: CreateIssueCredentialResponseOptions) { - return await this.openId4VcIssuerService.createIssueCredentialResponse(this.agentContext, options) - } - - /** - * Configures the enabled endpoints for the given router, as specified in @link https://openid.net/specs/openid-4-verifiable-credential-issuance-1_0.html - * - * @param router - The router to configure. - * @param endpointConfig - The endpoint configuration. - * @returns The configured router. - */ - public async configureRouter(router: Router, endpointConfig: IssuerEndpointConfig) { - return this.openId4VcIssuerService.configureRouter(this.agentContext, router, endpointConfig) + public async createCredentialResponse(options: CreateCredentialResponseOptions & { issuerId: string }) { + const { issuerId, ...rest } = options + const issuer = await this.openId4VcIssuerService.getByIssuerId(this.agentContext, issuerId) + return await this.openId4VcIssuerService.createCredentialResponse(this.agentContext, { ...rest, issuer }) } } diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts index 71e38a52ee..b65372616e 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts @@ -1,11 +1,19 @@ import type { OpenId4VcIssuerModuleConfigOptions } from './OpenId4VcIssuerModuleConfig' -import type { DependencyManager, Module } from '@aries-framework/core' +import type { IssuanceRequest } from './router/requestContext' +import type { AgentContext, DependencyManager, Module } from '@aries-framework/core' +import type { Router } from 'express' import { AgentConfig } from '@aries-framework/core' +import { getRequestContext } from '../shared/router' + import { OpenId4VcIssuerApi } from './OpenId4VcIssuerApi' import { OpenId4VcIssuerModuleConfig } from './OpenId4VcIssuerModuleConfig' import { OpenId4VcIssuerService } from './OpenId4VcIssuerService' +import { OpenId4VcIssuerRepository } from './repository/OpenId4VcIssuerRepository' +import { configureAccessTokenEndpoint, configureCredentialEndpoint, configureIssuerMetadataEndpoint } from './router' +import { importExpress } from './router/express' +import { getAgentContextForIssuerId } from './router/requestContext' /** * @public @@ -13,9 +21,14 @@ import { OpenId4VcIssuerService } from './OpenId4VcIssuerService' export class OpenId4VcIssuerModule implements Module { public readonly api = OpenId4VcIssuerApi public readonly config: OpenId4VcIssuerModuleConfig + public readonly router: Router public constructor(options: OpenId4VcIssuerModuleConfigOptions) { this.config = new OpenId4VcIssuerModuleConfig(options) + + // Initialize the router. The user still needs to register the router on their own express + // application. + this.router = importExpress().Router() } /** @@ -26,7 +39,7 @@ export class OpenId4VcIssuerModule implements Module { dependencyManager .resolve(AgentConfig) .logger.warn( - "The '@aries-framework/openid4vc-issuer' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages. Multi-Tenancy is not supported." + "The '@aries-framework/openid4vc' Issuer module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages. Multi-Tenancy is not supported." ) // Register config @@ -37,5 +50,67 @@ export class OpenId4VcIssuerModule implements Module { // Services dependencyManager.registerSingleton(OpenId4VcIssuerService) + + // Repository + dependencyManager.registerSingleton(OpenId4VcIssuerRepository) + } + + public async initialize(rootAgentContext: AgentContext): Promise { + this.configureRouter(rootAgentContext) + } + + /** + * Registers the endpoints on the router passed to this module. + */ + private configureRouter(rootAgentContext: AgentContext) { + const { Router, json, urlencoded } = importExpress() + + // We use separate context router and endpoint router. Context router handles the linking of the request + // to a specific agent context. Endpoint router only knows about a single context + const endpointRouter = Router() + + // parse application/x-www-form-urlencoded + this.router.use(urlencoded({ extended: false })) + // parse application/json + this.router.use(json()) + + this.router.param('issuerId', async (req: IssuanceRequest, _res, next, issuerId: string) => { + if (!issuerId) { + _res.status(404).send('Not found') + } + + let agentContext: AgentContext | undefined = undefined + + try { + agentContext = await getAgentContextForIssuerId(rootAgentContext, issuerId) + const issuerApi = agentContext.dependencyManager.resolve(OpenId4VcIssuerApi) + const issuer = await issuerApi.getByIssuerId(issuerId) + + req.requestContext = { + agentContext, + issuer, + } + } catch (error) { + // If the opening failed + await agentContext?.endSession() + return _res.status(404).send('Not found') + } + + next() + }) + + this.router.use('/:issuerId', endpointRouter) + + // Configure endpoints + configureIssuerMetadataEndpoint(endpointRouter) + configureAccessTokenEndpoint(endpointRouter, this.config.accessTokenEndpoint) + configureCredentialEndpoint(endpointRouter, this.config.credentialEndpoint) + + // FIXME: Will this be called when an error occurs / 404 is returned earlier on? + this.router.use(async (req: IssuanceRequest, _res, next) => { + const { agentContext } = getRequestContext(req) + await agentContext.endSession() + next() + }) } } diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts index 479aca1b6c..1017b9ee03 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts @@ -1,4 +1,4 @@ -import type { IssuerMetadata } from './OpenId4VcIssuerServiceOptions' +import type { AccessTokenEndpointConfig, CredentialEndpointConfig } from './OpenId4VcIssuerServiceOptions' import type { AgentContext } from '@aries-framework/core' import type { CNonceState, CredentialOfferSession, IStateManager, StateType, URIState } from '@sphereon/oid4vci-common' @@ -6,79 +6,106 @@ import { MemoryStates } from '@sphereon/oid4vci-issuer' export type StateManagerFactory = () => IStateManager +const DEFAULT_C_NONCE_EXPIRES_IN = 5 * 60 * 1000 // 5 minutes +const DEFAULT_TOKEN_EXPIRES_IN = 3 * 60 * 1000 // 3 minutes +const DEFAULT_PRE_AUTH_CODE_EXPIRES_IN = 3 * 60 * 1000 // 3 minutes + export interface OpenId4VcIssuerModuleConfigOptions { - issuerMetadata: IssuerMetadata + /** + * Base url at which the issuer endpoints will be hosted. All endpoints will be exposed with + * this path as prefix. + */ + baseUrl: string + + endpoints: { + // metadata endpoint does not have a config + // metadata?: MetadataEndpointConfig + credential: Optional + accessToken?: Optional< + AccessTokenEndpointConfig, + 'cNonceExpiresInSeconds' | 'endpointPath' | 'preAuthorizedCodeExpirationInSeconds' | 'tokenExpiresInSeconds' + > + } + + // FIXME: remove cNonceStateManagerFactory?: StateManagerFactory credentialOfferSessionManagerFactory?: StateManagerFactory uriStateManagerFactory?: StateManagerFactory - cNonceExpiresIn?: number - tokenExpiresIn?: number } +type Optional = Omit & Partial> + export class OpenId4VcIssuerModuleConfig { private options: OpenId4VcIssuerModuleConfigOptions private uriStateManagerMap: Map> private credentialOfferSessionManagerMap: Map> private cNonceStateManagerMap: Map> - private basePathMap: Map - private _cNonceExpiresIn: number - private _tokenExpiresIn: number - public constructor(options: OpenId4VcIssuerModuleConfigOptions) { - this.basePathMap = new Map() this.uriStateManagerMap = new Map() this.credentialOfferSessionManagerMap = new Map() this.cNonceStateManagerMap = new Map() - this._cNonceExpiresIn = options.cNonceExpiresIn ?? 5 * 60 * 1000 // 5 minutes - this._tokenExpiresIn = options.tokenExpiresIn ?? 3 * 60 * 1000 // 3 minutes this.options = options } - public get issuerMetadata(): IssuerMetadata { - return this.options.issuerMetadata - } - - public get cNonceExpiresIn(): number { - return this._cNonceExpiresIn - } - - public get tokenExpiresIn(): number { - return this._tokenExpiresIn + public get baseUrl() { + return this.options.baseUrl } - public getBasePath(agentContext: AgentContext): string { - return this.basePathMap.get(agentContext.contextCorrelationId) ?? '/' + /** + * Get the credential endpoint config, with default values set + */ + public get credentialEndpoint(): CredentialEndpointConfig { + // Use user supplied options, or return defaults. + const userOptions = this.options.endpoints.credential + + return { + ...userOptions, + endpointPath: userOptions.endpointPath ?? '/credential', + } } - public setBasePath(agentContext: AgentContext, basePath: string): void { - this.basePathMap.set(agentContext.contextCorrelationId, basePath) + /** + * Get the access token endpoint config, with default values set + */ + public get accessTokenEndpoint(): AccessTokenEndpointConfig { + // Use user supplied options, or return defaults. + const userOptions = this.options.endpoints.accessToken ?? {} + + return { + ...userOptions, + endpointPath: userOptions.endpointPath ?? '/token', + cNonceExpiresInSeconds: userOptions.cNonceExpiresInSeconds ?? DEFAULT_C_NONCE_EXPIRES_IN, + preAuthorizedCodeExpirationInSeconds: + userOptions.preAuthorizedCodeExpirationInSeconds ?? DEFAULT_PRE_AUTH_CODE_EXPIRES_IN, + tokenExpiresInSeconds: userOptions.tokenExpiresInSeconds ?? DEFAULT_TOKEN_EXPIRES_IN, + } } public getUriStateManager(agentContext: AgentContext) { - const val = this.uriStateManagerMap.get(agentContext.contextCorrelationId) - if (val) return val + const value = this.uriStateManagerMap.get(agentContext.contextCorrelationId) + if (value) return value - const newVal = this.options.uriStateManagerFactory?.() ?? new MemoryStates() - this.uriStateManagerMap.set(agentContext.contextCorrelationId, newVal) - return newVal + const newValue = this.options.uriStateManagerFactory?.() ?? new MemoryStates() + this.uriStateManagerMap.set(agentContext.contextCorrelationId, newValue) + return newValue } public getCredentialOfferSessionStateManager(agentContext: AgentContext) { - const val = this.credentialOfferSessionManagerMap.get(agentContext.contextCorrelationId) - if (val) return val + const value = this.credentialOfferSessionManagerMap.get(agentContext.contextCorrelationId) + if (value) return value - const newVal = this.options.credentialOfferSessionManagerFactory?.() ?? new MemoryStates() - this.credentialOfferSessionManagerMap.set(agentContext.contextCorrelationId, newVal) - return newVal + const newValue = this.options.credentialOfferSessionManagerFactory?.() ?? new MemoryStates() + this.credentialOfferSessionManagerMap.set(agentContext.contextCorrelationId, newValue) + return newValue } public getCNonceStateManager(agentContext: AgentContext) { - const val = this.cNonceStateManagerMap.get(agentContext.contextCorrelationId) - if (val) return val + const value = this.cNonceStateManagerMap.get(agentContext.contextCorrelationId) + if (value) return value - const newVal = this.options.cNonceStateManagerFactory?.() ?? new MemoryStates() - this.cNonceStateManagerMap.set(agentContext.contextCorrelationId, newVal) - return newVal + const newValue = this.options.cNonceStateManagerFactory?.() ?? new MemoryStates() + this.cNonceStateManagerMap.set(agentContext.contextCorrelationId, newValue) + return newValue } } diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts index 6806fd702d..e599cb61f5 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts @@ -1,142 +1,267 @@ import type { AuthorizationCodeFlowConfig, - CreateCredentialOfferAndRequestOptions, - CreateIssueCredentialResponseOptions, - CredentialOfferAndRequest, - CredentialSupported, - IssuerEndpointConfig, + CreateCredentialOfferOptions, + CreateCredentialResponseOptions, + CreateIssuerOptions, + CredentialHolderBinding, + CredentialOffer, IssuerMetadata, - OfferedCredential, + OpenId4VciSignCredential, PreAuthorizedCodeFlowConfig, } from './OpenId4VcIssuerServiceOptions' -import type { IssuanceRequest } from './router/OpenId4VcIEndpointConfiguration' -import type { OfferedCredentialWithMetadata } from '../openid4vc-holder/reception/utils/IssuerMetadataUtils' -import type { - AgentContext, - DidDocument, - JwaSignatureAlgorithm, - VerificationMethod, - W3cVerifiableCredential, -} from '@aries-framework/core' -import type { SdJwtCredential, SdJwtVcModule } from '@aries-framework/sd-jwt-vc' +import type { ReferencedOfferedCredentialWithMetadata } from '../openid4vc-holder/reception/utils/IssuerMetadataUtils' +import type { AgentContext, DidDocument, Jwk, W3cSignCredentialOptions } from '@aries-framework/core' +import type { SdJwtVcModule, SdJwtVcSignOptions } from '@aries-framework/sd-jwt-vc' import type { CredentialOfferPayloadV1_0_11, CredentialRequestV1_0_11, Grant, JWTVerifyCallback, + CredentialSupported, } from '@sphereon/oid4vci-common' import type { CredentialDataSupplier, CredentialDataSupplierArgs, + CredentialIssuanceInput, CredentialSignerCallback, } from '@sphereon/oid4vci-issuer' import type { ICredential } from '@sphereon/ssi-types' -import type { NextFunction, Response, Router } from 'express' import { - AgentContextProvider, + getJwkFromJson, + KeyType, + utils, AriesFrameworkError, - ClaimFormat, DidsApi, - InjectionSymbols, JsonTransformer, JwsService, Jwt, - Logger, W3cCredential, W3cCredentialService, equalsIgnoreOrder, getApiForModuleByName, getJwkFromKey, getKeyFromVerificationMethod, - inject, injectable, + joinUriParts, } from '@aries-framework/core' import { IssueStatus } from '@sphereon/oid4vci-common' import { VcIssuerBuilder } from '@sphereon/oid4vci-issuer' -import bodyParser from 'body-parser' import { OpenIdCredentialFormatProfile } from '../openid4vc-holder' -import { getOfferedCredentialsWithMetadata } from '../openid4vc-holder/reception/utils/IssuerMetadataUtils' -import { getEndpointUrl, initializeAgentFromContext, getRequestContext } from '../shared/router' +import { + OfferedCredentialType, + getOfferedCredentialsWithMetadata, +} from '../openid4vc-holder/reception/utils/IssuerMetadataUtils' import { getSphereonW3cVerifiableCredential } from '../shared/transform' -import { getProofTypeFromVerificationMethod } from '../shared/utils' import { OpenId4VcIssuerModuleConfig } from './OpenId4VcIssuerModuleConfig' -import { configureAccessTokenEndpoint, configureCredentialEndpoint } from './router/OpenId4VcIEndpointConfiguration' -import { configureIssuerMetadataEndpoint } from './router/metadataEndpoint' +import { OpenId4VcIssuerRecord } from './repository/OpenId4VcIssuerRecord' +import { OpenId4VcIssuerRepository } from './repository/OpenId4VcIssuerRepository' +import { storeIssuerIdForContextCorrelationId } from './router/requestContext' + +const W3cOpenId4VcFormats = [ + OpenIdCredentialFormatProfile.JwtVcJson, + OpenIdCredentialFormatProfile.JwtVcJsonLd, + OpenIdCredentialFormatProfile.LdpVc, +] /** * @internal */ @injectable() export class OpenId4VcIssuerService { - private logger: Logger private w3cCredentialService: W3cCredentialService private jwsService: JwsService - private _openId4VcIssuerModuleConfig: OpenId4VcIssuerModuleConfig - private agentContextProvider: AgentContextProvider - - public get openId4VcIssuerModuleConfig() { - return this._openId4VcIssuerModuleConfig - } - - public get issuerMetadata() { - return this.openId4VcIssuerModuleConfig.issuerMetadata - } + private openId4VcIssuerConfig: OpenId4VcIssuerModuleConfig + private openId4VcIssuerRepository: OpenId4VcIssuerRepository public constructor( - @inject(InjectionSymbols.Logger) logger: Logger, - @inject(InjectionSymbols.AgentContextProvider) agentContextProvider: AgentContextProvider, - openId4VcIssuerModuleConfig: OpenId4VcIssuerModuleConfig, w3cCredentialService: W3cCredentialService, - jwsService: JwsService + jwsService: JwsService, + openId4VcIssuerConfig: OpenId4VcIssuerModuleConfig, + openId4VcIssuerRepository: OpenId4VcIssuerRepository ) { - this.agentContextProvider = agentContextProvider this.w3cCredentialService = w3cCredentialService - this.logger = logger - this._openId4VcIssuerModuleConfig = openId4VcIssuerModuleConfig this.jwsService = jwsService + this.openId4VcIssuerConfig = openId4VcIssuerConfig + this.openId4VcIssuerRepository = openId4VcIssuerRepository } - public expandEndpointsWithBase(agentContext: AgentContext): IssuerMetadata { - const issuerMetadata = this.issuerMetadata - const basePath = this.openId4VcIssuerModuleConfig.getBasePath(agentContext) + public getIssuerMetadata(agentContext: AgentContext, issuerRecord: OpenId4VcIssuerRecord): IssuerMetadata { + const config = agentContext.dependencyManager.resolve(OpenId4VcIssuerModuleConfig) + const issuerUrl = joinUriParts([config.baseUrl, issuerRecord.issuerId]) + const issuerMetadata = { + issuerUrl, + tokenEndpoint: joinUriParts([issuerUrl, config.accessTokenEndpoint.endpointPath]), + credentialEndpoint: joinUriParts([issuerUrl, config.credentialEndpoint.endpointPath]), + credentialsSupported: issuerRecord.credentialsSupported, + issuerDisplay: issuerRecord.display, + } satisfies IssuerMetadata + + return issuerMetadata + } - const credentialIssuer = getEndpointUrl(issuerMetadata.issuerBaseUrl, basePath) - const tokenEndpoint = getEndpointUrl(credentialIssuer, basePath, issuerMetadata.tokenEndpointPath) - const credentialEndpoint = getEndpointUrl(credentialIssuer, basePath, issuerMetadata.credentialEndpointPath) + public async createCredentialOffer( + agentContext: AgentContext, + options: CreateCredentialOfferOptions & { issuer: OpenId4VcIssuerRecord } + ): Promise { + const { preAuthorizedCodeFlowConfig, authorizationCodeFlowConfig, issuer, offeredCredentials } = options + + const vcIssuer = this.getVcIssuer(agentContext, issuer) + + // this checks if the structure of the credentials is correct + // it throws an error if a offered credential cannot be found in the credentialsSupported + getOfferedCredentialsWithMetadata(offeredCredentials, vcIssuer.issuerMetadata.credentials_supported) + + const { uri, session } = await vcIssuer.createCredentialOfferURI({ + grants: await this.getGrantsFromConfig(agentContext, preAuthorizedCodeFlowConfig, authorizationCodeFlowConfig), + credentials: offeredCredentials, + credentialOfferUri: options.credentialOfferUri, + scheme: options.scheme ?? 'https', + baseUri: options.baseUri, + }) return { - ...issuerMetadata, - issuerBaseUrl: credentialIssuer, - tokenEndpointPath: tokenEndpoint, - credentialEndpointPath: credentialEndpoint, + credentialOfferPayload: session.credentialOffer.credential_offer, + credentialOfferUri: uri, } } + public async getCredentialOfferFromUri(agentContext: AgentContext, uri: string) { + const { credentialOfferSessionId, credentialOfferSession } = await this.getCredentialOfferSessionFromUri( + agentContext, + uri + ) + + credentialOfferSession.lastUpdatedAt = +new Date() + credentialOfferSession.status = IssueStatus.OFFER_URI_RETRIEVED + await this.openId4VcIssuerConfig + .getCredentialOfferSessionStateManager(agentContext) + .set(credentialOfferSessionId, credentialOfferSession) + + return credentialOfferSession.credentialOffer.credential_offer + } + + public async createCredentialResponse( + agentContext: AgentContext, + options: CreateCredentialResponseOptions & { issuer: OpenId4VcIssuerRecord } + ) { + const { credentialRequest, issuer } = options + if (!credentialRequest.proof) throw new AriesFrameworkError('No proof defined in the credentialRequest.') + + const vcIssuer = this.getVcIssuer(agentContext, issuer) + const issueCredentialResponse = await vcIssuer.issueCredential({ + credentialRequest, + // FIXME: move this to top-level config (or at least not endpoint config) + tokenExpiresIn: this.openId4VcIssuerConfig.accessTokenEndpoint.tokenExpiresInSeconds, + + // This can just be combined with signing callback right? + credentialDataSupplier: this.getCredentialDataSupplier(agentContext, options), + newCNonce: undefined, + responseCNonce: undefined, + }) + + if (!issueCredentialResponse.credential) { + throw new AriesFrameworkError('No credential found in the issueCredentialResponse.') + } + + if (issueCredentialResponse.acceptance_token) { + throw new AriesFrameworkError('Acceptance token not yet supported.') + } + + return issueCredentialResponse + } + + public async getAllIssuers(agentContext: AgentContext) { + return this.openId4VcIssuerRepository.getAll(agentContext) + } + + public async getByIssuerId(agentContext: AgentContext, issuerId: string) { + return this.openId4VcIssuerRepository.getByIssuerId(agentContext, issuerId) + } + + public async updateIssuer(agentContext: AgentContext, issuer: OpenId4VcIssuerRecord) { + return this.openId4VcIssuerRepository.update(agentContext, issuer) + } + + public async createIssuer(agentContext: AgentContext, options: CreateIssuerOptions) { + // TODO: ideally we can store additional data with a key, such as: + // - createdAt + // - purpose + const accessTokenSignerKey = await agentContext.wallet.createKey({ + keyType: KeyType.Ed25519, + }) + const openId4VcIssuer = new OpenId4VcIssuerRecord({ + issuerId: utils.uuid(), + display: options.display, + accessTokenPublicKeyFingerprint: accessTokenSignerKey.fingerprint, + credentialsSupported: options.credentialsSupported, + }) + + await this.openId4VcIssuerRepository.save(agentContext, openId4VcIssuer) + await storeIssuerIdForContextCorrelationId(agentContext, openId4VcIssuer.issuerId) + return openId4VcIssuer + } + + public async rotateAccessTokenSigningKey(agentContext: AgentContext, issuer: OpenId4VcIssuerRecord) { + const accessTokenSignerKey = await agentContext.wallet.createKey({ + keyType: KeyType.Ed25519, + }) + + // TODO: ideally we can remove the previous key + issuer.accessTokenPublicKeyFingerprint = accessTokenSignerKey.fingerprint + await this.openId4VcIssuerRepository.update(agentContext, issuer) + } + + private async getCredentialOfferSessionFromUri(agentContext: AgentContext, uri: string) { + const uriState = await this.openId4VcIssuerConfig.getUriStateManager(agentContext).get(uri) + if (!uriState) throw new AriesFrameworkError(`Credential offer uri '${uri}' not found.`) + + const credentialOfferSessionId = uriState.preAuthorizedCode ?? uriState.issuerState + if (!credentialOfferSessionId) { + throw new AriesFrameworkError( + `Credential offer uri '${uri}' is not associated with a preAuthorizedCode or issuerState.` + ) + } + + const credentialOfferSession = await this.openId4VcIssuerConfig + .getCredentialOfferSessionStateManager(agentContext) + .get(credentialOfferSessionId) + if (!credentialOfferSession) + throw new AriesFrameworkError( + `Credential offer session for '${uri}' with id '${credentialOfferSessionId}' not found.` + ) + + return { credentialOfferSessionId, credentialOfferSession } + } + private getJwtVerifyCallback = (agentContext: AgentContext): JWTVerifyCallback => { return async (opts) => { const { jwt } = opts const { header, payload } = Jwt.fromSerializedJwt(jwt) - const { alg, kid } = header + const { alg, kid, jwk: jwkJson } = header - // kid: JOSE Header containing the key ID. If the Credential shall be bound to a DID, - // the kid refers to a DID URL which identifies a particular key in the DID Document that - // the Credential shall be bound to. MUST NOT be present if jwk or x5c is present. - if (!kid) throw new AriesFrameworkError('No KID is present for verifying the proof of possession.') + if (kid && jwkJson) { + throw new AriesFrameworkError('Either kid or jwk must be present, but not both') + } - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - const didDocument = await didsApi.resolveDidDocument(kid) - const verificationMethod = didDocument.dereferenceKey(kid, ['authentication', 'assertionMethod']) - const key = getKeyFromVerificationMethod(verificationMethod) - const jwk = getJwkFromKey(key) - - if (!jwk.supportsSignatureAlgorithm(alg)) { - throw new AriesFrameworkError( - `The signature algorithm '${alg}' is not supported by keys of type '${jwk.keyType}'.` - ) + let jwk: Jwk + let didDocument: DidDocument | undefined = undefined + if (kid) { + if (!kid.startsWith('did:')) { + throw new AriesFrameworkError("Only kid with 'did:' prefix is supported for JWT") + } + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + didDocument = await didsApi.resolveDidDocument(kid) + const verificationMethod = didDocument.dereferenceKey(kid, ['authentication', 'assertionMethod']) + const key = getKeyFromVerificationMethod(verificationMethod) + jwk = getJwkFromKey(key) + } else if (jwkJson) { + jwk = getJwkFromJson(jwkJson) + } else { + throw new AriesFrameworkError('Either kid or jwk must be present') } const { isValid } = await this.jwsService.verifyJws(agentContext, { @@ -149,40 +274,33 @@ export class OpenId4VcIssuerService { return { jwt: { header, payload: payload.toJson() }, kid, - did: didDocument.id, + jwk: jwkJson, + did: kid, alg, didDocument, } } } - private getVcIssuer(agentContext: AgentContext) { - const issuerMetadata = this.expandEndpointsWithBase(agentContext) - const { - issuerBaseUrl: credentialIssuer, - tokenEndpointPath: tokenEndpoint, - credentialEndpointPath: credentialEndpoint, - credentialsSupported, - } = issuerMetadata + private getVcIssuer(agentContext: AgentContext, issuer: OpenId4VcIssuerRecord) { + const issuerMetadata = this.getIssuerMetadata(agentContext, issuer) const builder = new VcIssuerBuilder() - .withCredentialIssuer(credentialIssuer.toString()) - .withCredentialEndpoint(credentialEndpoint.toString()) - .withTokenEndpoint(tokenEndpoint.toString()) - .withCredentialsSupported(credentialsSupported) - .withCNonceExpiresIn(this.openId4VcIssuerModuleConfig.cNonceExpiresIn) - .withCNonceStateManager(this.openId4VcIssuerModuleConfig.getCNonceStateManager(agentContext)) - .withCredentialOfferStateManager( - this.openId4VcIssuerModuleConfig.getCredentialOfferSessionStateManager(agentContext) - ) - .withCredentialOfferURIStateManager(this.openId4VcIssuerModuleConfig.getUriStateManager(agentContext)) + .withCredentialIssuer(issuerMetadata.issuerUrl) + .withCredentialEndpoint(issuerMetadata.credentialEndpoint) + .withTokenEndpoint(issuerMetadata.tokenEndpoint) + .withCredentialsSupported(issuerMetadata.credentialsSupported) + // FIXME: need to create persistent state managers + .withCNonceStateManager(this.openId4VcIssuerConfig.getCNonceStateManager(agentContext)) + .withCredentialOfferStateManager(this.openId4VcIssuerConfig.getCredentialOfferSessionStateManager(agentContext)) + .withCredentialOfferURIStateManager(this.openId4VcIssuerConfig.getUriStateManager(agentContext)) .withJWTVerifyCallback(this.getJwtVerifyCallback(agentContext)) .withCredentialSignerCallback(() => { - throw new AriesFrameworkError('this should never ba called') + throw new AriesFrameworkError('Credential signer callback should be overwritten. This is a no-op') }) - if (issuerMetadata.authorizationServerUrl) { - builder.withAuthorizationServer(issuerMetadata.authorizationServerUrl.toString()) + if (issuerMetadata.authorizationServer) { + builder.withAuthorizationServer(issuerMetadata.authorizationServer) } if (issuerMetadata.issuerDisplay) { @@ -218,79 +336,21 @@ export class OpenId4VcIssuerService { return grants } - public async createCredentialOfferAndRequest( - agentContext: AgentContext, - offeredCredentials: OfferedCredential[], - options: CreateCredentialOfferAndRequestOptions - ): Promise { - const { preAuthorizedCodeFlowConfig, authorizationCodeFlowConfig } = options - - // this checks if the structure of the credentials is correct - // it throws an error if a offered credential cannot be found in the credentialsSupported - getOfferedCredentialsWithMetadata(offeredCredentials, this.issuerMetadata.credentialsSupported) - - const vcIssuer = this.getVcIssuer(agentContext) - - const { uri, session } = await vcIssuer.createCredentialOfferURI({ - grants: await this.getGrantsFromConfig(agentContext, preAuthorizedCodeFlowConfig, authorizationCodeFlowConfig), - credentials: offeredCredentials, - credentialOfferUri: options.credentialOfferUri, - scheme: options.scheme ?? 'https', - baseUri: options.baseUri ?? '', - // credentialDefinition, - }) - - return { - credentialOfferPayload: session.credentialOffer.credential_offer, - credentialOfferRequest: uri, - } - } - - private async getCredentialOfferSessionFromUri(agentContext: AgentContext, uri: string) { - const uriState = await this.openId4VcIssuerModuleConfig.getUriStateManager(agentContext).get(uri) - if (!uriState) throw new AriesFrameworkError(`Credential offer uri '${uri}' not found.`) - - const credentialOfferSessionId = uriState.preAuthorizedCode ?? uriState.issuerState - if (!credentialOfferSessionId) { - throw new AriesFrameworkError( - `Credential offer uri '${uri}' is not associated with a preAuthorizedCode or issuerState.` - ) - } - - const credentialOfferSession = await this.openId4VcIssuerModuleConfig - .getCredentialOfferSessionStateManager(agentContext) - .get(credentialOfferSessionId) - if (!credentialOfferSession) - throw new AriesFrameworkError( - `Credential offer session for '${uri}' with id '${credentialOfferSessionId}' not found.` - ) - - return { credentialOfferSessionId, credentialOfferSession } - } - - public async getCredentialOfferFromUri(agentContext: AgentContext, uri: string) { - const { credentialOfferSessionId, credentialOfferSession } = await this.getCredentialOfferSessionFromUri( - agentContext, - uri - ) - - credentialOfferSession.lastUpdatedAt = +new Date() - credentialOfferSession.status = IssueStatus.OFFER_URI_RETRIEVED - await this.openId4VcIssuerModuleConfig - .getCredentialOfferSessionStateManager(agentContext) - .set(credentialOfferSessionId, credentialOfferSession) - - return credentialOfferSession.credentialOffer.credential_offer - } - private findOfferedCredentialsMatchingRequest( credentialOffer: CredentialOfferPayloadV1_0_11, credentialRequest: CredentialRequestV1_0_11, credentialsSupported: CredentialSupported[] - ): OfferedCredentialWithMetadata[] { + ): ReferencedOfferedCredentialWithMetadata[] { const offeredCredentials = getOfferedCredentialsWithMetadata(credentialOffer.credentials, credentialsSupported) - return offeredCredentials.filter((offeredCredential) => { + // NOTE: we only support referenced offered credentials + // Filter out inline offers (should not be present in the first case as we don't support them at issuance) + const referencedOfferedCredentials = offeredCredentials.filter( + (offeredCredential): offeredCredential is ReferencedOfferedCredentialWithMetadata => + offeredCredential.offerType === OfferedCredentialType.CredentialSupported + ) + + return referencedOfferedCredentials.filter((offeredCredential) => { if (offeredCredential.format !== credentialRequest.format) return false if (credentialRequest.format === OpenIdCredentialFormatProfile.JwtVcJson) { @@ -301,105 +361,126 @@ export class OpenId4VcIssuerService { ) { return equalsIgnoreOrder(offeredCredential.types, credentialRequest.credential_definition.types) } else if (credentialRequest.format === OpenIdCredentialFormatProfile.SdJwtVc) { - return equalsIgnoreOrder(offeredCredential.types, [credentialRequest.credential_definition.vct]) + return equalsIgnoreOrder(offeredCredential.types, [credentialRequest.vct]) } }) } - private getSdJwtVcCredentialSigningCallback = (agentContext: AgentContext): CredentialSignerCallback => { - return async (opts) => { - const { credential } = opts - + private getSdJwtVcCredentialSigningCallback = ( + agentContext: AgentContext, + options: SdJwtVcSignOptions + ): CredentialSignerCallback => { + return async () => { const sdJwtVcApi = getApiForModuleByName(agentContext, 'SdJwtVcModule') if (!sdJwtVcApi) throw new AriesFrameworkError(`Could not find the SdJwtVcApi`) - const { compact } = await sdJwtVcApi.signCredential(credential as any) - return compact as any + const { compact } = await sdJwtVcApi.sign(options) + + return compact } } private getW3cCredentialSigningCallback = ( agentContext: AgentContext, - issuerVerificationMethod: VerificationMethod + options: W3cSignCredentialOptions ): CredentialSignerCallback => { return async (opts) => { - const { credential, jwtVerifyResult, format } = opts + const { jwtVerifyResult } = opts - const { alg, kid, didDocument: holderDidDocument } = jwtVerifyResult + // FIXME: how certain can we be that the key is verified to + // be in the did document? Where is the did resolved? + // I think in the jwtVerifyCallback we provide + const { kid, didDocument: holderDidDocument } = jwtVerifyResult if (!kid) throw new AriesFrameworkError('Missing Kid. Cannot create the holder binding') if (!holderDidDocument) throw new AriesFrameworkError('Missing did document. Cannot create the holder binding.') - // If the Credential shall be bound to a DID, the kid refers to a DID URL which identifies a - // particular key in the DID Document that the Credential shall be bound to. - const holderVerificationMethod = holderDidDocument.dereferenceKey(kid, ['assertionMethod']) - - let signed: W3cVerifiableCredential - if (format === OpenIdCredentialFormatProfile.JwtVcJson || format === OpenIdCredentialFormatProfile.JwtVcJsonLd) { - signed = await this.w3cCredentialService.signCredential(agentContext, { - format: ClaimFormat.JwtVc, - credential: W3cCredential.fromJson(credential), - verificationMethod: issuerVerificationMethod.id, - alg: alg as JwaSignatureAlgorithm, - }) - } else if (format === OpenIdCredentialFormatProfile.LdpVc) { - signed = await this.w3cCredentialService.signCredential(agentContext, { - format: ClaimFormat.LdpVc, - credential: W3cCredential.fromJson(credential), - verificationMethod: issuerVerificationMethod.id, - proofPurpose: 'assertionMethod', - proofType: getProofTypeFromVerificationMethod(agentContext, holderVerificationMethod), - }) - } else { - throw new AriesFrameworkError(`Unsupported credential format '${format}' for W3C credential signing callback.`) + // Set the binding on the first credential subject if not set yet + // on any subject + if (!options.credential.credentialSubjectIds.includes(holderDidDocument.id)) { + const credentialSubject = Array.isArray(options.credential.credentialSubject) + ? options.credential.credentialSubject[0] + : options.credential.credentialSubject + credentialSubject.id = holderDidDocument.id } + const signed = await this.w3cCredentialService.signCredential(agentContext, options) + return getSphereonW3cVerifiableCredential(signed) } } + private async getHolderBindingFromRequest(agentContext: AgentContext, credentialRequest: CredentialRequestV1_0_11) { + if (!credentialRequest.proof?.jwt) throw new AriesFrameworkError('Received a credential request without a proof') + + const jwt = Jwt.fromSerializedJwt(credentialRequest.proof.jwt) + + if (jwt.header.kid) { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + await didsApi.resolveDidDocument(jwt.header.kid) + return { + method: 'did', + didUrl: jwt.header.kid, + } satisfies CredentialHolderBinding + } else if (jwt.header.jwk) { + return { + method: 'jwk', + jwk: getJwkFromJson(jwt.header.jwk), + } satisfies CredentialHolderBinding + } else { + throw new AriesFrameworkError('Either kid or jwk must be present in credential request proof header') + } + } + private getCredentialDataSupplier = ( agentContext: AgentContext, - credential: SdJwtCredential | W3cCredential, - issuerVerificationMethod: VerificationMethod + options: CreateCredentialResponseOptions & { issuer: OpenId4VcIssuerRecord } ): CredentialDataSupplier => { return async (args: CredentialDataSupplierArgs) => { const { credentialRequest, credentialOffer } = args + const issuerMetadata = this.getIssuerMetadata(agentContext, options.issuer) const offeredCredentialsMatchingRequest = this.findOfferedCredentialsMatchingRequest( credentialOffer.credential_offer, credentialRequest, - this.issuerMetadata.credentialsSupported + issuerMetadata.credentialsSupported ) if (offeredCredentialsMatchingRequest.length === 0) { throw new AriesFrameworkError('No offered credentials match the credential request.') } - if (credential instanceof W3cCredential) { - if ( - credentialRequest.format !== OpenIdCredentialFormatProfile.JwtVcJson && - credentialRequest.format !== OpenIdCredentialFormatProfile.JwtVcJsonLd && - credentialRequest.format !== OpenIdCredentialFormatProfile.LdpVc - ) { - throw new AriesFrameworkError( - `The credential to be issued does not match the request. Cannot issue a W3cCredential if the client expects a credential of format '${credentialRequest.format}'.` - ) - } - const issuedCredentialMatchesRequest = offeredCredentialsMatchingRequest.find((offeredCredential) => { - return equalsIgnoreOrder(offeredCredential.types, credential.type) + if (offeredCredentialsMatchingRequest.length > 1) { + agentContext.config.logger.debug( + 'Multiple credentials from credentials supported matching request, picking first one.' + ) + } + + let signOptions = options.credential + if (!signOptions) { + const holderBinding = await this.getHolderBindingFromRequest(agentContext, credentialRequest) + signOptions = await this.openId4VcIssuerConfig.credentialEndpoint.credentialRequestToCredentialMapper({ + agentContext, + holderBinding, + + credentialOffer, + credentialRequest, + + credentialsSupported: offeredCredentialsMatchingRequest.map((o) => o.credentialSupported), }) + } - if (!issuedCredentialMatchesRequest) { + if (isW3cSignCredentialOptions(signOptions)) { + if (!W3cOpenId4VcFormats.includes(credentialRequest.format as OpenIdCredentialFormatProfile)) { throw new AriesFrameworkError( - `The types of the offered credentials do not match the types of the requested credential. Requested '${credential.type}'.` + `The credential to be issued does not match the request. Cannot issue a W3cCredential if the client expects a credential of format '${credentialRequest.format}'.` ) } return { format: credentialRequest.format, - credential: JsonTransformer.toJSON(credential) as ICredential, - signCallback: this.getW3cCredentialSigningCallback(agentContext, issuerVerificationMethod), + credential: JsonTransformer.toJSON(signOptions.credential) as ICredential, + signCallback: this.getW3cCredentialSigningCallback(agentContext, signOptions), } } else { if (credentialRequest.format !== OpenIdCredentialFormatProfile.SdJwtVc) { @@ -407,115 +488,23 @@ export class OpenId4VcIssuerService { `Invalid credential format. Expected '${OpenIdCredentialFormatProfile.SdJwtVc}', received '${credentialRequest.format}'.` ) } - if (credentialRequest.credential_definition.vct !== credential.payload.type) { + if (credentialRequest.vct !== signOptions.payload.vct) { throw new AriesFrameworkError( - `The types of the offered credentials do not match the types of the requested credential. Offered '${credential.payload.vct}' Requested '${credentialRequest.credential_definition.vct}'.` + `The types of the offered credentials do not match the types of the requested credential. Offered '${signOptions.payload.vct}' Requested '${credentialRequest.vct}'.` ) } return { format: credentialRequest.format, - credential: credential as any, // TODO: sdjwt - signCallback: this.getSdJwtVcCredentialSigningCallback(agentContext), + // NOTE: we don't use the credential value here as we pass the credential directly to the singer + credential: null as unknown as CredentialIssuanceInput, + signCallback: this.getSdJwtVcCredentialSigningCallback(agentContext, signOptions), } } } } +} - public async createIssueCredentialResponse( - agentContext: AgentContext, - options: CreateIssueCredentialResponseOptions - ) { - const { credentialRequest, credential, verificationMethod } = options - if (!credentialRequest.proof) throw new AriesFrameworkError('No proof defined in the credentialRequest.') - - const vcIssuer = this.getVcIssuer(agentContext) - const issueCredentialResponse = await vcIssuer.issueCredential({ - credentialRequest, - tokenExpiresIn: this.openId4VcIssuerModuleConfig.tokenExpiresIn, - cNonceExpiresIn: this.openId4VcIssuerModuleConfig.cNonceExpiresIn, - credentialDataSupplier: this.getCredentialDataSupplier(agentContext, credential, verificationMethod), - credential: undefined, - newCNonce: undefined, - credentialDataSupplierInput: undefined, - responseCNonce: undefined, - }) - - if (!issueCredentialResponse.credential) { - throw new AriesFrameworkError('No credential found in the issueCredentialResponse.') - } - - if (issueCredentialResponse.acceptance_token) { - throw new AriesFrameworkError('Acceptance token not yet supported.') - } - - return issueCredentialResponse - } - - public configureRouter = ( - initializationContext: AgentContext, - router: Router, - endpointConfig: IssuerEndpointConfig - ) => { - const { basePath } = endpointConfig - this.openId4VcIssuerModuleConfig.setBasePath(initializationContext, basePath) - - // parse application/x-www-form-urlencoded - router.use(bodyParser.urlencoded({ extended: false })) - // parse application/json - router.use(bodyParser.json()) - // initialize the agent and set the request context - router.use(async (req: IssuanceRequest, _res: Response, next: NextFunction) => { - const agentContext = await initializeAgentFromContext( - initializationContext.contextCorrelationId, - this.agentContextProvider - ) - - req.requestContext = { - agentContext, - openId4vcIssuerService: agentContext.dependencyManager.resolve(OpenId4VcIssuerService), - logger: agentContext.dependencyManager.resolve(InjectionSymbols.Logger), - } - - next() - }) - - if (endpointConfig.metadataEndpointConfig?.enabled) { - const wellKnownPath = `/.well-known/openid-credential-issuer` - configureIssuerMetadataEndpoint(router, wellKnownPath) - - const endpointPath = getEndpointUrl(this.issuerMetadata.issuerBaseUrl, basePath, wellKnownPath) - this.logger.info(`[OID4VCI] Metadata endpoint running at '${endpointPath}'.`) - } - - if (endpointConfig.accessTokenEndpointConfig?.enabled) { - const accessTokenEndpointPath = this.issuerMetadata.tokenEndpointPath - configureAccessTokenEndpoint(router, accessTokenEndpointPath, { - ...endpointConfig.accessTokenEndpointConfig, - cNonceExpiresIn: this.openId4VcIssuerModuleConfig.cNonceExpiresIn, - tokenExpiresIn: this.openId4VcIssuerModuleConfig.tokenExpiresIn, - }) - - const endpointPath = getEndpointUrl(this.issuerMetadata.issuerBaseUrl, basePath, accessTokenEndpointPath) - this.logger.info(`[OID4VCI] Token endpoint running at '${endpointPath}'.`) - } - - if (endpointConfig.credentialEndpointConfig?.enabled) { - const credentialEndpointPath = this.issuerMetadata.credentialEndpointPath - configureCredentialEndpoint(router, credentialEndpointPath, { - ...endpointConfig.credentialEndpointConfig, - }) - - const endpointUrl = getEndpointUrl(this.issuerMetadata.issuerBaseUrl, basePath, credentialEndpointPath) - this.logger.info(`[OID4VCI] Credential endpoint running at '${endpointUrl}'.`) - } - - router.use(async (req: IssuanceRequest, _res, next) => { - const { agentContext } = getRequestContext(req) - await agentContext.endSession() - next() - }) - - return router - } +function isW3cSignCredentialOptions(credential: OpenId4VciSignCredential): credential is W3cSignCredentialOptions { + return 'credential' in credential && credential.credential instanceof W3cCredential } diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts index 45e1320227..0bbe1d8a96 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts @@ -1,22 +1,8 @@ -import type { AgentContext, VerificationMethod, W3cCredential } from '@aries-framework/core' -import type { SdJwtCredential } from '@aries-framework/sd-jwt-vc' -import type { - CNonceState, - CredentialOfferFormat, - CredentialOfferPayloadV1_0_11, - CredentialOfferSession, - CredentialRequestV1_0_11, - CredentialSupported, - MetadataDisplay, - ProofOfPossession, -} from '@sphereon/oid4vci-common' - -export type { MetadataDisplay, ProofOfPossession, CredentialSupported, CredentialOfferFormat } - -// If the entry is an object, the object contains the data related to a certain credential type -// the Wallet MAY request. Each object MUST contain a format Claim determining the format -// and further parameters characterizing by the format of the credential to be requested. -export type OfferedCredential = CredentialOfferFormat | string +import type { OpenId4VcIssuerRecordProps } from './repository/OpenId4VcIssuerRecord' +import type { OpenId4VciCredentialOffer, OpenId4VciCredentialRequest, OpenId4VciCredentialSupported } from '../shared' +import type { AgentContext, Jwk, W3cSignCredentialOptions } from '@aries-framework/core' +import type { SdJwtVcSignOptions } from '@aries-framework/sd-jwt-vc' +import type { CredentialOfferPayloadV1_0_11, CredentialSupported, MetadataDisplay } from '@sphereon/oid4vci-common' export type PreAuthorizedCodeFlowConfig = { preAuthorizedCode?: string @@ -29,16 +15,24 @@ export type AuthorizationCodeFlowConfig = { export type IssuerMetadata = { // The Credential Issuer's identifier. (URL using the https scheme) - issuerBaseUrl: string - credentialEndpointPath: string - tokenEndpointPath: string - authorizationServerUrl?: string - issuerDisplay?: MetadataDisplay + issuerUrl: string + credentialEndpoint: string + tokenEndpoint: string + authorizationServer?: string + issuerDisplay?: MetadataDisplay[] credentialsSupported: CredentialSupported[] } -export interface CreateCredentialOfferAndRequestOptions { +export type CreateIssuerOptions = Pick + +export interface CreateCredentialOfferOptions { + // NOTE: v11 of OID4VCI supports both inline and referenced (to credentials_supported.id) credential offers. + // In draft 12 the inline credential offers have been removed and to make the migration to v12 easier + // we only support referenced credentials in an offer + offeredCredentials: string[] + + // FIXME: can we simplify this? // The scheme used for the credentialIssuer. Default is https scheme?: 'http' | 'https' | 'openid-credential-offer' | string @@ -51,34 +45,43 @@ export interface CreateCredentialOfferAndRequestOptions { credentialOfferUri?: string } -export type CredentialOfferAndRequest = { +// FIXME: this needs to be renamed, but will class with OpenId4VciCredentialOffer +// Probably needs to be specific `XXReturn` type +export type CredentialOffer = { credentialOfferPayload: CredentialOfferPayloadV1_0_11 - credentialOfferRequest: string -} - -export interface CreateIssueCredentialResponseOptions { - credentialRequest: CredentialRequestV1_0_11 - credential: W3cCredential | SdJwtCredential - verificationMethod: VerificationMethod + credentialOfferUri: string } -export { CredentialRequestV1_0_11 } +export interface CreateCredentialResponseOptions { + credentialRequest: OpenId4VciCredentialRequest -export { CredentialResponse } from '@sphereon/oid4vci-common' + /** + * You can optionally provide the input data for signing the credential. + * If not provided the `credentialRequestToCredentialMapper` from the module + * config will be called with needed data to construct the credential + * signing payload + */ + // FIXME: credential.credential is not nice + credential?: OpenId4VciSignCredential +} -export interface MetadataEndpointConfig { +export type MetadataEndpointConfig = { /** * Configures the router to expose the metadata endpoint. */ - enabled: boolean + enabled: true } -export interface AccessTokenEndpointConfig { +export type AccessTokenEndpointConfig = { /** - * Configures the router to expose the access token endpoint. + * The path at which the token endpoint should be made available. Note that it will be + * hosted at a subpath to take into account multiple tenants and issuers. + * + * @default /token */ - enabled: boolean + endpointPath: string + // FIXME: rename, more specific /** * The minimum amount of time in seconds that the client SHOULD wait between polling requests to the Token Endpoint in the Pre-Authorized Code Flow. * If no value is provided, clients MUST use 5 as the default. @@ -86,45 +89,90 @@ export interface AccessTokenEndpointConfig { interval?: number /** - * The verification method to be used to sign access token. + * The maximum amount of time in seconds that the pre-authorized code is valid. + * @default 360 (5 minutes) // FIXME: what should be the default value */ - verificationMethod: VerificationMethod + preAuthorizedCodeExpirationInSeconds: number /** - * The maximum amount of time in seconds that the pre-authorized code is valid. + * The time after which the cNonce from the access token response will + * expire. + * + * @default 360 (5 minutes) // FIXME: what should be the default value? + */ + cNonceExpiresInSeconds: number + + /** + * The time after which the token will expire. + * + * @default 360 (5 minutes) // FIXME: what should be the default value? + */ + tokenExpiresInSeconds: number +} + +export type CredentialEndpointConfig = { + /** + * The path at which the credential endpoint should be made available. Note that it will be + * hosted at a subpath to take into account multiple tenants and issuers. + * + * @default /credential */ - preAuthorizedCodeExpirationDuration: number + endpointPath: string + + /** + * A function mapping a credential request to the credential to be issued. + */ + credentialRequestToCredentialMapper: CredentialRequestToCredentialMapper } +// FIXME: Flows: +// - provide credential data at time of offer creation +// - provide credential data dynamically using this method export type CredentialRequestToCredentialMapper = (options: { agentContext: AgentContext - credentialRequest: CredentialRequestV1_0_11 - holderDid: string - holderDidUrl: string - cNonceState: CNonceState - credentialOfferSession: CredentialOfferSession -}) => Promise - -export interface CredentialEndpointConfig { + /** - * Configures the router to expose the credential endpoint. + * The credential request received from the wallet */ - enabled: boolean + credentialRequest: OpenId4VciCredentialRequest /** - * The verification method to be used to sign the credential. + * The offer associated with the credential request */ - verificationMethod: VerificationMethod + credentialOffer: OpenId4VciCredentialOffer /** - * A function mapping a credential request to the credential to be issued. + * Verified key binding material that should be included in the credential + * + * Can either be bound to did or a JWK (in case of for ex. SD-JWT) */ - credentialRequestToCredentialMapper: CredentialRequestToCredentialMapper + holderBinding: CredentialHolderBinding + + /** + * The credentials supported entries from the issuer metadata that were offered + * and match the incoming request + * + * NOTE: in v12 this will probably become a single entry, as it will be matched on id + */ + credentialsSupported: OpenId4VciCredentialSupported[] +}) => Promise + +// FIXME: can we make these interfaces more uniform or is it okay +// to have quite some differences between them? I think the nice +// thing here is that they are based on the interface from the +// w3c and sd-jwt services. However in that case you could also +// ask why not just require the signed credential as output +// as you can then just call the services yourself. +export type OpenId4VciSignCredential = SdJwtVcSignOptions | W3cSignCredentialOptions + +export type CredentialHolderDidBinding = { + method: 'did' + didUrl: string } -export interface IssuerEndpointConfig { - basePath: string - metadataEndpointConfig?: MetadataEndpointConfig - accessTokenEndpointConfig?: AccessTokenEndpointConfig - credentialEndpointConfig?: CredentialEndpointConfig +export type CredentialHolderJwkBinding = { + method: 'jwk' + jwk: Jwk } + +export type CredentialHolderBinding = CredentialHolderDidBinding | CredentialHolderJwkBinding diff --git a/packages/openid4vc/src/openid4vc-issuer/index.ts b/packages/openid4vc/src/openid4vc-issuer/index.ts index f99285d628..ed7cf40ba0 100644 --- a/packages/openid4vc/src/openid4vc-issuer/index.ts +++ b/packages/openid4vc/src/openid4vc-issuer/index.ts @@ -3,3 +3,4 @@ export * from './OpenId4VcIssuerModule' export * from './OpenId4VcIssuerService' export * from './OpenId4VcIssuerModuleConfig' export * from './OpenId4VcIssuerServiceOptions' +export { OpenId4VcIssuerRecord, OpenId4VcIssuerRecordProps, OpenId4VcIssuerRecordTags } from './repository' diff --git a/packages/openid4vc/src/openid4vc-issuer/router/OpenId4VcIEndpointConfiguration.ts b/packages/openid4vc/src/openid4vc-issuer/router/OpenId4VcIEndpointConfiguration.ts deleted file mode 100644 index 5100104736..0000000000 --- a/packages/openid4vc/src/openid4vc-issuer/router/OpenId4VcIEndpointConfiguration.ts +++ /dev/null @@ -1,114 +0,0 @@ -import type { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' -import type { - AccessTokenEndpointConfig, - CredentialEndpointConfig, - MetadataEndpointConfig, -} from '../OpenId4VcIssuerServiceOptions' -import type { AgentContext, Logger } from '@aries-framework/core' -import type { CredentialRequestV1_0_11 } from '@sphereon/oid4vci-common' -import type { Request, Response, Router } from 'express' - -import { AriesFrameworkError, DidsApi, Jwt } from '@aries-framework/core' - -import { getRequestContext, sendErrorResponse } from './../../shared/router' -import { handleTokenRequest, verifyTokenRequest } from './accessTokenEndpoint' - -export interface IssuanceRequestContext { - agentContext: AgentContext - openId4vcIssuerService: OpenId4VcIssuerService - logger: Logger -} - -export interface IssuanceRequest extends Request { - requestContext?: IssuanceRequestContext -} - -export type InternalMetadataEndpointConfig = MetadataEndpointConfig - -export interface InternalAccessTokenEndpointConfig extends AccessTokenEndpointConfig { - cNonceExpiresIn: number - tokenExpiresIn: number -} - -export function configureAccessTokenEndpoint( - router: Router, - pathname: string, - config: InternalAccessTokenEndpointConfig -) { - const { preAuthorizedCodeExpirationDuration } = config - router.post(pathname, verifyTokenRequest({ preAuthorizedCodeExpirationDuration }), handleTokenRequest(config)) -} - -export type InternalCredentialEndpointConfig = CredentialEndpointConfig - -export function configureCredentialEndpoint( - router: Router, - pathname: string, - config: InternalCredentialEndpointConfig -) { - const { credentialRequestToCredentialMapper, verificationMethod } = config - - router.post(pathname, async (request: IssuanceRequest, response: Response) => { - const requestContext = getRequestContext(request) - const { agentContext, openId4vcIssuerService, logger } = requestContext - - try { - const credentialRequest = request.body as CredentialRequestV1_0_11 - - if (!credentialRequest.proof?.jwt) throw new AriesFrameworkError('Received a credential request without a proof') - const jwt = Jwt.fromSerializedJwt(credentialRequest.proof?.jwt) - - const kid = jwt.header.kid - if (!kid) throw new AriesFrameworkError('Received a credential request without a kid') - - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - const didDocument = await didsApi.resolveDidDocument(kid) - const holderDid = didDocument.id - - const requestNonce = jwt.payload.additionalClaims.nonce - if (!requestNonce || typeof requestNonce !== 'string') { - throw new AriesFrameworkError(`Received a credential request without a valid nonce. ${requestNonce}`) - } - - const cNonceState = await openId4vcIssuerService.openId4VcIssuerModuleConfig - .getCNonceStateManager(agentContext) - .get(requestNonce) - - const credentialOfferSessionId = cNonceState?.preAuthorizedCode ?? cNonceState?.issuerState - - if (!cNonceState || !credentialOfferSessionId) { - throw new AriesFrameworkError( - `Request nonce '${requestNonce}' is not associated with a preAuthorizedCode or issuerState.` - ) - } - - const credentialOfferSession = await openId4vcIssuerService.openId4VcIssuerModuleConfig - .getCredentialOfferSessionStateManager(agentContext) - .get(credentialOfferSessionId) - - if (!credentialOfferSession) - throw new AriesFrameworkError( - `Credential offer session for request nonce '${requestNonce}' with id '${credentialOfferSessionId}' not found.` - ) - - const credential = await credentialRequestToCredentialMapper({ - agentContext, - credentialRequest, - holderDid, - holderDidUrl: kid, - cNonceState, - credentialOfferSession, - }) - - const issueCredentialResponse = await openId4vcIssuerService.createIssueCredentialResponse(agentContext, { - credentialRequest, - verificationMethod, - credential, - }) - - return response.send(issueCredentialResponse) - } catch (e) { - sendErrorResponse(response, logger, 500, 'invalid_request', e) - } - }) -} diff --git a/packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts b/packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts index 5b7ef28c51..f77cf5be57 100644 --- a/packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts @@ -1,15 +1,16 @@ -import type { InternalAccessTokenEndpointConfig, IssuanceRequest } from './OpenId4VcIEndpointConfiguration' -import type { AgentContext, JwkJson, VerificationMethod } from '@aries-framework/core' +import type { IssuanceRequest } from './requestContext' +import type { AccessTokenEndpointConfig } from '../OpenId4VcIssuerServiceOptions' +import type { AgentContext } from '@aries-framework/core' import type { JWTSignerCallback } from '@sphereon/oid4vci-common' -import type { NextFunction, Response } from 'express' +import type { NextFunction, Response, Router } from 'express' import { + getJwkFromKey, AriesFrameworkError, JwsService, JwtPayload, getJwkClassFromKeyType, - getJwkFromJson, - getKeyFromVerificationMethod, + Key, } from '@aries-framework/core' import { GrantTypes, @@ -20,40 +21,44 @@ import { import { assertValidAccessTokenRequest, createAccessTokenResponse } from '@sphereon/oid4vci-issuer' import { getRequestContext, sendErrorResponse } from '../../shared/router' +import { OpenId4VcIssuerModuleConfig } from '../OpenId4VcIssuerModuleConfig' +import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' -const getJwtSignerCallback = ( - agentContext: AgentContext, - verificationMethod: VerificationMethod -): JWTSignerCallback => { +const getJwtSignerCallback = (agentContext: AgentContext, signerPublicKey: Key): JWTSignerCallback => { return async (jwt, _kid) => { - if (_kid) throw new AriesFrameworkError('Kid should not be supplied externally.') + if (_kid) { + throw new AriesFrameworkError('Kid should not be supplied externally.') + } + if (jwt.header.kid || jwt.header.jwk) { + throw new AriesFrameworkError('kid or jwk should not be present in access token header before signing') + } const jwsService = agentContext.dependencyManager.resolve(JwsService) - const key = getKeyFromVerificationMethod(verificationMethod) - const alg = getJwkClassFromKeyType(key.keyType)?.supportedSignatureAlgorithms[0] - if (!alg) throw new AriesFrameworkError(`No supported signature algorithms for key type: ${key.keyType}`) - - const jwk = jwt.header.jwk ? getJwkFromJson(jwt.header.jwk as JwkJson) : undefined + const alg = getJwkClassFromKeyType(signerPublicKey.keyType)?.supportedSignatureAlgorithms[0] + if (!alg) { + throw new AriesFrameworkError(`No supported signature algorithms for key type: ${signerPublicKey.keyType}`) + } - const signedJwt: string = await jwsService.createJwsCompact(agentContext, { - protectedHeaderOptions: { ...jwt.header, jwk, kid: verificationMethod.id, alg }, + const jwk = getJwkFromKey(signerPublicKey) + const signedJwt = await jwsService.createJwsCompact(agentContext, { + protectedHeaderOptions: { ...jwt.header, jwk, alg }, payload: new JwtPayload(jwt.payload), - key, + key: signerPublicKey, }) return signedJwt } } -export const handleTokenRequest = (config: InternalAccessTokenEndpointConfig) => { - const { tokenExpiresIn, cNonceExpiresIn, interval } = config +export const handleTokenRequest = (config: AccessTokenEndpointConfig) => { + const { tokenExpiresInSeconds, cNonceExpiresInSeconds, interval } = config return async (request: IssuanceRequest, response: Response) => { response.set({ 'Cache-Control': 'no-store', Pragma: 'no-cache' }) const requestContext = getRequestContext(request) - const { agentContext, openId4vcIssuerService, logger } = requestContext + const { agentContext, issuer } = requestContext if (request.body.grant_type !== GrantTypes.PRE_AUTHORIZED_CODE) { return response.status(400).json({ @@ -62,46 +67,64 @@ export const handleTokenRequest = (config: InternalAccessTokenEndpointConfig) => }) } + const openId4VcIssuerConfig = agentContext.dependencyManager.resolve(OpenId4VcIssuerModuleConfig) + const openId4VcIssuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService) + const issuerMetadata = openId4VcIssuerService.getIssuerMetadata(agentContext, issuer) + const accessTokenSigningKey = Key.fromFingerprint(issuer.accessTokenPublicKeyFingerprint) + try { const accessTokenResponse = await createAccessTokenResponse(request.body, { - credentialOfferSessions: - openId4vcIssuerService.openId4VcIssuerModuleConfig.getCredentialOfferSessionStateManager(agentContext), - tokenExpiresIn, - accessTokenIssuer: openId4vcIssuerService.expandEndpointsWithBase(agentContext).issuerBaseUrl, + credentialOfferSessions: openId4VcIssuerConfig.getCredentialOfferSessionStateManager(agentContext), + tokenExpiresIn: tokenExpiresInSeconds, + accessTokenIssuer: issuerMetadata.issuerUrl, cNonce: await agentContext.wallet.generateNonce(), - cNonceExpiresIn, - cNonces: openId4vcIssuerService.openId4VcIssuerModuleConfig.getCNonceStateManager(agentContext), - accessTokenSignerCallback: getJwtSignerCallback(agentContext, config.verificationMethod), + cNonceExpiresIn: cNonceExpiresInSeconds, + cNonces: openId4VcIssuerConfig.getCNonceStateManager(agentContext), + accessTokenSignerCallback: getJwtSignerCallback(agentContext, accessTokenSigningKey), interval, }) return response.status(200).json(accessTokenResponse) } catch (error) { - sendErrorResponse(response, logger, 400, TokenErrorResponse.invalid_request, error) + sendErrorResponse(response, agentContext.config.logger, 400, TokenErrorResponse.invalid_request, error) } } } -export const verifyTokenRequest = (options: { preAuthorizedCodeExpirationDuration: number }) => { - const { preAuthorizedCodeExpirationDuration } = options +export const verifyTokenRequest = (options: { preAuthorizedCodeExpirationInSeconds: number }) => { + const { preAuthorizedCodeExpirationInSeconds } = options return async (request: IssuanceRequest, response: Response, next: NextFunction) => { - const requestContext = getRequestContext(request) - const { agentContext, openId4vcIssuerService, logger } = requestContext + const { agentContext } = getRequestContext(request) + const openId4VcIssuerConfig = agentContext.dependencyManager.resolve(OpenId4VcIssuerModuleConfig) try { await assertValidAccessTokenRequest(request.body, { // we use seconds instead of milliseconds for consistency - expirationDuration: preAuthorizedCodeExpirationDuration * 1000, - credentialOfferSessions: - openId4vcIssuerService.openId4VcIssuerModuleConfig.getCredentialOfferSessionStateManager(agentContext), + expirationDuration: preAuthorizedCodeExpirationInSeconds * 1000, + credentialOfferSessions: openId4VcIssuerConfig.getCredentialOfferSessionStateManager(agentContext), }) } catch (error) { if (error instanceof TokenError) { - sendErrorResponse(response, logger, error.statusCode, error.responseError + error.getDescription(), error) + sendErrorResponse( + response, + agentContext.config.logger, + error.statusCode, + error.responseError + error.getDescription(), + error + ) } else { - sendErrorResponse(response, logger, 400, TokenErrorResponse.invalid_request, error) + sendErrorResponse(response, agentContext.config.logger, 400, TokenErrorResponse.invalid_request, error) } } return next() } } + +export function configureAccessTokenEndpoint(router: Router, config: AccessTokenEndpointConfig) { + const { preAuthorizedCodeExpirationInSeconds } = config + router.post( + config.endpointPath, + verifyTokenRequest({ preAuthorizedCodeExpirationInSeconds }), + handleTokenRequest(config) + ) +} diff --git a/packages/openid4vc/src/openid4vc-issuer/router/credentialEndpoint.ts b/packages/openid4vc/src/openid4vc-issuer/router/credentialEndpoint.ts new file mode 100644 index 0000000000..36fe74c421 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/router/credentialEndpoint.ts @@ -0,0 +1,27 @@ +import type { IssuanceRequest } from './requestContext' +import type { CredentialEndpointConfig } from '../OpenId4VcIssuerServiceOptions' +import type { CredentialRequestV1_0_11 } from '@sphereon/oid4vci-common' +import type { Router, Response } from 'express' + +import { getRequestContext, sendErrorResponse } from '../../shared/router' +import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' + +export function configureCredentialEndpoint(router: Router, config: CredentialEndpointConfig) { + router.post(config.endpointPath, async (request: IssuanceRequest, response: Response) => { + const requestContext = getRequestContext(request) + const { agentContext, issuer } = requestContext + const openId4VcIssuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService) + + try { + const credentialRequest = request.body as CredentialRequestV1_0_11 + const issueCredentialResponse = await openId4VcIssuerService.createCredentialResponse(agentContext, { + issuer, + credentialRequest, + }) + + return response.send(issueCredentialResponse) + } catch (e) { + sendErrorResponse(response, agentContext.config.logger, 500, 'invalid_request', e) + } + }) +} diff --git a/packages/openid4vc/src/openid4vc-issuer/router/express.ts b/packages/openid4vc/src/openid4vc-issuer/router/express.ts new file mode 100644 index 0000000000..43bdcf12fa --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/router/express.ts @@ -0,0 +1,12 @@ +import type { default as Express } from 'express' + +export function importExpress() { + try { + // NOTE: 'express' is added as a peer-dependency, and is required when using this module + // eslint-disable-next-line import/no-extraneous-dependencies, @typescript-eslint/no-var-requires + const express = require('express') as typeof Express + return express + } catch (error) { + throw new Error('Express must be installed as a peer dependency') + } +} diff --git a/packages/openid4vc/src/openid4vc-issuer/router/index.ts b/packages/openid4vc/src/openid4vc-issuer/router/index.ts new file mode 100644 index 0000000000..836352a1d7 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/router/index.ts @@ -0,0 +1,4 @@ +export { configureAccessTokenEndpoint } from './accessTokenEndpoint' +export { configureCredentialEndpoint } from './credentialEndpoint' +export { configureIssuerMetadataEndpoint } from './metadataEndpoint' +export { IssuanceRequest, IssuanceRequestContext } from './requestContext' diff --git a/packages/openid4vc/src/openid4vc-issuer/router/metadataEndpoint.ts b/packages/openid4vc/src/openid4vc-issuer/router/metadataEndpoint.ts index ec9659928a..482b6ea245 100644 --- a/packages/openid4vc/src/openid4vc-issuer/router/metadataEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-issuer/router/metadataEndpoint.ts @@ -1,25 +1,29 @@ -import type { IssuanceRequest } from './OpenId4VcIEndpointConfiguration' -import type { CredentialIssuerMetadata, CredentialSupported } from '@sphereon/oid4vci-common' +import type { IssuanceRequest } from './requestContext' +import type { CredentialIssuerMetadata } from '@sphereon/oid4vci-common' import type { Router, Response } from 'express' import { getRequestContext, sendErrorResponse } from '../../shared/router' +import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' -export function configureIssuerMetadataEndpoint(router: Router, pathname: string) { - router.get(pathname, (_request: IssuanceRequest, response: Response) => { - const { agentContext, openId4vcIssuerService, logger } = getRequestContext(_request) +export function configureIssuerMetadataEndpoint(router: Router) { + router.get('.well-known/openid-credential-issuer', (_request: IssuanceRequest, response: Response) => { + const { agentContext, issuer } = getRequestContext(_request) + + const openId4VcIssuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService) try { - const metadata = openId4vcIssuerService.expandEndpointsWithBase(agentContext) - const transformedMetadata: CredentialIssuerMetadata = { - credential_issuer: metadata.issuerBaseUrl, - token_endpoint: metadata.tokenEndpointPath, - credential_endpoint: metadata.credentialEndpointPath, - authorization_server: metadata.authorizationServerUrl, - credentials_supported: metadata.credentialsSupported as CredentialSupported[], - display: metadata.issuerDisplay ? [metadata.issuerDisplay] : undefined, - } + const issuerMetadata = openId4VcIssuerService.getIssuerMetadata(agentContext, issuer) + const transformedMetadata = { + credential_issuer: issuerMetadata.issuerUrl, + token_endpoint: issuerMetadata.tokenEndpoint, + credential_endpoint: issuerMetadata.credentialEndpoint, + authorization_server: issuerMetadata.authorizationServer, + credentials_supported: issuerMetadata.credentialsSupported, + display: issuerMetadata.issuerDisplay, + } satisfies CredentialIssuerMetadata + response.status(200).json(transformedMetadata) } catch (e) { - sendErrorResponse(response, logger, 500, 'invalid_request', e) + sendErrorResponse(response, agentContext.config.logger, 500, 'invalid_request', e) } }) } diff --git a/packages/openid4vc/src/openid4vc-issuer/router/requestContext.ts b/packages/openid4vc/src/openid4vc-issuer/router/requestContext.ts new file mode 100644 index 0000000000..e1b4cccdd3 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-issuer/router/requestContext.ts @@ -0,0 +1,61 @@ +import type { RequestContext } from '../../shared/router' +import type { OpenId4VcIssuerRecord } from '../repository/OpenId4VcIssuerRecord' +import type { AgentContext, AgentContextProvider } from '@aries-framework/core' +import type { TenantsModule } from '@aries-framework/tenants' +import type { Request } from 'express' + +import { InjectionSymbols, getApiForModuleByName } from '@aries-framework/core' + +// Type is currently same as base request context +export type IssuanceRequestContext = RequestContext & { issuer: OpenId4VcIssuerRecord } +export interface IssuanceRequest extends Request { + requestContext?: IssuanceRequestContext +} + +const OPENID4VC_ISSUER_IDS_METADATA_KEY = '_openid4vc/openId4VcIssuerIds' + +export async function getAgentContextForIssuerId(rootAgentContext: AgentContext, issuerId: string) { + // Check if multi-tenancy is enabled, and if so find the associated multi-tenant record + // This is a bit hacky as it uses the tenants module to store the openid4vc issuer id + // but this way we don't have to expose the contextCorrelationId in the issuer metadata + const tenantsApi = getApiForModuleByName(rootAgentContext, 'TenantsApi') + if (tenantsApi) { + const [tenant] = await tenantsApi.findTenantsByQuery({ + [OPENID4VC_ISSUER_IDS_METADATA_KEY]: [issuerId], + }) + + if (tenant) { + const agentContextProvider = rootAgentContext.dependencyManager.resolve( + InjectionSymbols.AgentContextProvider + ) + await agentContextProvider.getAgentContextForContextCorrelationId(tenant.id) + } + } + + return rootAgentContext +} + +/** + * Store the issuer id associated with a context correlation id. If multi-tenancy is not used + * this method won't do anything as we can just use the issuer from the default context. However + * if multi-tenancy is used, we will store the issuer id in the tenant record metadata so it can + * be queried when a request comes in for the specific issuer id. + * + * The reason for doing this is that we don't want to expose the context correlation id in the + * issuer metadata url, as it is then possible to see exactly which issuers are registered under + * the same agent. + */ +export async function storeIssuerIdForContextCorrelationId(agentContext: AgentContext, issuerId: string) { + // It's kind of hacky, but we add support for the tenants module specifically here to map an issuerId to + // a specific tenant. Otherwise we have to expose /:contextCorrelationId/:issuerId in all the public URLs + // which is of course not so nice. + const tenantsApi = getApiForModuleByName(agentContext, 'TenantsApi') + // We don't want to query the tenant record if the current context is the root context + if (tenantsApi && tenantsApi.rootAgentContext.contextCorrelationId !== agentContext.contextCorrelationId) { + const tenantRecord = await tenantsApi.getTenantById(agentContext.contextCorrelationId) + + const openId4VcIssuerIds = tenantRecord.metadata.get(OPENID4VC_ISSUER_IDS_METADATA_KEY) ?? [] + tenantRecord.metadata.set(OPENID4VC_ISSUER_IDS_METADATA_KEY, [...openId4VcIssuerIds, issuerId]) + await tenantsApi.updateTenant(tenantRecord) + } +} diff --git a/packages/openid4vc/src/shared/router.ts b/packages/openid4vc/src/shared/router.ts index 374862fb8f..e1354e63fb 100644 --- a/packages/openid4vc/src/shared/router.ts +++ b/packages/openid4vc/src/shared/router.ts @@ -1,8 +1,11 @@ -import type { AgentContextProvider, Logger } from '@aries-framework/core' +import type { AgentContext, Logger } from '@aries-framework/core' import type { Response, Request } from 'express' -import { AriesFrameworkError, WalletApi } from '@aries-framework/core' -import path from 'path' +import { AriesFrameworkError } from '@aries-framework/core' + +export interface RequestContext { + agentContext: AgentContext +} export function sendErrorResponse(response: Response, logger: Logger, code: number, message: string, error: unknown) { const error_description = @@ -14,7 +17,7 @@ export function sendErrorResponse(response: Response, logger: Logger, code: numb return response.status(code).json(body) } -export function getRequestContext( +export function getRequestContext( request: T ): NonNullable { const requestContext = request.requestContext @@ -22,38 +25,3 @@ export function getRequestContext /** - * Default of sha2-256 will be used + * Default of sha2-256 will be used if not provided */ hashingAlgorithm?: HashName } From 82b53ebf4b85cd2a5d235698101a265efd4567b9 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Thu, 11 Jan 2024 15:11:12 +0700 Subject: [PATCH 095/115] issuance working Signed-off-by: Timo Glastra --- demo-openid/src/Holder.ts | 27 +- demo-openid/src/HolderInquirer.ts | 20 +- demo-openid/src/Issuer.ts | 216 ++++++++------- demo-openid/src/IssuerInquirer.ts | 5 +- package.json | 4 +- packages/core/src/crypto/JwsService.ts | 21 +- packages/core/src/crypto/jose/jwt/Jwt.ts | 2 +- .../models/W3cJsonLdVerifiableCredential.ts | 4 + packages/core/src/utils/path.ts | 16 +- packages/openid4vc/package.json | 13 +- .../openid4vc-holder/OpenId4VcHolderApi.ts | 9 +- .../reception/OpenId4VciHolderService.ts | 225 +++++++++------ .../OpenId4VciHolderServiceOptions.ts | 30 +- .../reception/utils/IssuerMetadataUtils.ts | 38 +-- .../openid4vc-issuer/OpenId4VcIssuerModule.ts | 23 +- .../OpenId4VcIssuerModuleConfig.ts | 15 + .../OpenId4VcIssuerService.ts | 146 ++++++---- .../OpenId4VcIssuerServiceOptions.ts | 26 +- .../__tests__/openId4vc-issuer-module.test.ts | 40 ++- .../router/metadataEndpoint.ts | 2 +- .../openid4vc-issuer/router/requestContext.ts | 15 +- .../shared/models/CredentialHolderBinding.ts | 13 + packages/openid4vc/src/shared/models/index.ts | 8 + packages/openid4vc/src/shared/router.ts | 4 +- .../openid4vc/tests/openid4vc.e2e.test.ts | 261 +++++++++--------- packages/openid4vc/tests/utils.ts | 30 +- packages/openid4vc/tests/utilsVci.ts | 3 + packages/sd-jwt-vc/src/SdJwtVcService.ts | 8 +- 28 files changed, 722 insertions(+), 502 deletions(-) create mode 100644 packages/openid4vc/src/shared/models/CredentialHolderBinding.ts diff --git a/demo-openid/src/Holder.ts b/demo-openid/src/Holder.ts index 3240c2125c..c53050ecd5 100644 --- a/demo-openid/src/Holder.ts +++ b/demo-openid/src/Holder.ts @@ -1,4 +1,3 @@ -import type { W3cCredentialRecord } from '@aries-framework/core' import type { OfferedCredentialWithMetadata, ResolvedPresentationRequest, @@ -6,8 +5,9 @@ import type { } from '@aries-framework/openid4vc' import { AskarModule } from '@aries-framework/askar' +import { W3cJwtVerifiableCredential, W3cJsonLdVerifiableCredential } from '@aries-framework/core' import { OpenId4VcHolderModule } from '@aries-framework/openid4vc' -import { SdJwtVcModule, type SdJwtVcRecord } from '@aries-framework/sd-jwt-vc' +import { SdJwtVcModule } from '@aries-framework/sd-jwt-vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { BaseAgent } from './BaseAgent' @@ -41,20 +41,25 @@ export class Holder extends BaseAgent> resolvedCredentialOffer: ResolvedCredentialOffer, credentialsToRequest: OfferedCredentialWithMetadata[] ) { - const credentialRecords = await this.agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + const credentials = await this.agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( resolvedCredentialOffer, { credentialsToRequest, - proofOfPossessionVerificationMethodResolver: async () => this.verificationMethod, + // TODO: add jwk support for holder binding + credentialBindingResolver: async () => ({ + method: 'did', + didUrl: this.verificationMethod.id, + }), } ) - const storedCredentials: (W3cCredentialRecord | SdJwtVcRecord)[] = await Promise.all( - credentialRecords.map((record) => { - if (record.type === 'W3cCredentialRecord') { - return this.agent.w3cCredentials.storeCredential({ credential: record.credential }) + const storedCredentials = await Promise.all( + credentials.map((credential) => { + if (credential instanceof W3cJwtVerifiableCredential || credential instanceof W3cJsonLdVerifiableCredential) { + return this.agent.w3cCredentials.storeCredential({ credential }) + } else { + return this.agent.modules.sdJwtVc.store(credential.compact) } - return this.agent.modules.sdJwtVc.storeCredential(record) }) ) @@ -71,10 +76,10 @@ export class Holder extends BaseAgent> } public async acceptPresentationRequest( - resolvedPresentationReuest: ResolvedPresentationRequest, + resolvedPresentationRequest: ResolvedPresentationRequest, submissionEntryIndexes: number[] ) { - const { presentationRequest, presentationSubmission } = resolvedPresentationReuest + const { presentationRequest, presentationSubmission } = resolvedPresentationRequest const submissionResult = await this.agent.modules.openId4VcHolder.acceptPresentationRequest(presentationRequest, { submission: presentationSubmission, submissionEntryIndexes, diff --git a/demo-openid/src/HolderInquirer.ts b/demo-openid/src/HolderInquirer.ts index 52815db3f3..b37f27fa03 100644 --- a/demo-openid/src/HolderInquirer.ts +++ b/demo-openid/src/HolderInquirer.ts @@ -117,18 +117,18 @@ export class HolderInquirer extends BaseInquirer { this.resolvedCredentialOffer.offeredCredentials ) - console.log(greenText(`Received and stored the following credentials.`)) - console.log( - greenText( - credentials - .map((credential) => { - if (credential.type === 'W3cCredentialRecord') - return credential.credential.type.join(', ') + `, CredentialType: 'W3CVerifiableCredential'` - else return credential.sdJwtVc.payload.type + `, CredentialType: 'SdJwtVc'` - }) - .join('\n') + const credentialTypes = await Promise.all( + credentials.map((credential) => + credential.type === 'W3cCredentialRecord' + ? `${credential.credential.type.join(', ')}, CredentialType: W3cVerifiableCredential` + : this.holder.agent.modules.sdJwtVc + .fromCompact(credential.compactSdJwtVc) + .then((a) => `${a.prettyClaims.vct}, CredentialType: SdJwtVc`) ) ) + + console.log(greenText(`Received and stored the following credentials.`)) + console.log(greenText(credentialTypes.join('\n'))) } public async resolveProofRequest() { diff --git a/demo-openid/src/Issuer.ts b/demo-openid/src/Issuer.ts index a6f281f0d7..db70b69cf5 100644 --- a/demo-openid/src/Issuer.ts +++ b/demo-openid/src/Issuer.ts @@ -1,15 +1,23 @@ +import type { DidKey } from '@aries-framework/core' import type { + CredentialHolderBinding, + CredentialHolderDidBinding, CredentialRequestToCredentialMapper, - CredentialSupported, - OfferedCredential, - IssuerEndpointConfig, + OpenId4VciCredentialSupportedWithId, + OpenId4VcIssuerRecord, } from '@aries-framework/openid4vc' -import type e from 'express' import { AskarModule } from '@aries-framework/askar' -import { W3cCredential, W3cCredentialSubject, W3cIssuer, w3cDate } from '@aries-framework/core' +import { + parseDid, + AriesFrameworkError, + W3cCredential, + W3cCredentialSubject, + W3cIssuer, + w3cDate, +} from '@aries-framework/core' import { OpenId4VcIssuerModule, OpenIdCredentialFormatProfile } from '@aries-framework/openid4vc' -import { SdJwtCredential, SdJwtVcModule } from '@aries-framework/sd-jwt-vc' +import { SdJwtVcModule } from '@aries-framework/sd-jwt-vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { Router } from 'express' @@ -20,125 +28,139 @@ export const universityDegreeCredential = { id: 'UniversityDegreeCredential', format: OpenIdCredentialFormatProfile.JwtVcJson, types: ['VerifiableCredential', 'UniversityDegreeCredential'], -} satisfies CredentialSupported & { id: string } +} satisfies OpenId4VciCredentialSupportedWithId export const openBadgeCredential = { id: 'OpenBadgeCredential', format: OpenIdCredentialFormatProfile.JwtVcJson, types: ['VerifiableCredential', 'OpenBadgeCredential'], -} satisfies CredentialSupported & { id: string } +} satisfies OpenId4VciCredentialSupportedWithId export const universityDegreeCredentialSdJwt = { id: 'UniversityDegreeCredential-sdjwt', format: OpenIdCredentialFormatProfile.SdJwtVc, - credential_definition: { - vct: 'UniversityDegreeCredential', - }, -} satisfies CredentialSupported & { id: string } + vct: 'UniversityDegreeCredential', +} satisfies OpenId4VciCredentialSupportedWithId export const credentialsSupported = [ universityDegreeCredential, openBadgeCredential, universityDegreeCredentialSdJwt, -] satisfies CredentialSupported[] - -function getOpenIdIssuerModules() { - return { - sdJwtVc: new SdJwtVcModule(), - askar: new AskarModule({ ariesAskar }), - openId4VcIssuer: new OpenId4VcIssuerModule({ - issuerMetadata: { - issuerBaseUrl: 'http://localhost:2000', - tokenEndpointPath: '/token', - credentialEndpointPath: '/credentials', - credentialsSupported, - }, - }), - } as const -} +] satisfies OpenId4VciCredentialSupportedWithId[] -export class Issuer extends BaseAgent> { - public constructor(port: number, name: string) { - super({ port, name, modules: getOpenIdIssuerModules() }) - } +function getCredentialRequestToCredentialMapper({ + issuerDidKey, +}: { + issuerDidKey: DidKey +}): CredentialRequestToCredentialMapper { + return async ({ holderBinding, credentialsSupported }) => { + const credentialSupported = credentialsSupported[0] - public static async build(): Promise { - const issuer = new Issuer(2000, 'OpenId4VcIssuer ' + Math.random().toString()) - await issuer.initializeAgent('96213c3d7fc8d4d6754c7a0fd969598f') - - return issuer - } + if (credentialSupported.id === universityDegreeCredential.id) { + assertDidBasedHolderBinding(holderBinding) - public async configureRouter(): Promise { - const endpointConfig: IssuerEndpointConfig = { - basePath: '/', - metadataEndpointConfig: { enabled: true }, - accessTokenEndpointConfig: { - enabled: true, - verificationMethod: this.verificationMethod, - preAuthorizedCodeExpirationDuration: 100, - }, - credentialEndpointConfig: { - enabled: true, - verificationMethod: this.verificationMethod, - credentialRequestToCredentialMapper: await this.getCredentialRequestToCredentialMapper(), - }, - } - - const router = await this.agent.modules.openId4VcIssuer.configureRouter(Router(), endpointConfig) - this.app.use('/', router) - return router - } - - public getCredentialRequestToCredentialMapper(): CredentialRequestToCredentialMapper { - return async ({ credentialRequest, holderDid, holderDidUrl }) => { - if ( - credentialRequest.format === 'jwt_vc_json' && - credentialRequest.types.includes('UniversityDegreeCredential') - ) { - return new W3cCredential({ + return { + credential: new W3cCredential({ type: universityDegreeCredential.types, - issuer: new W3cIssuer({ id: this.did }), - credentialSubject: new W3cCredentialSubject({ id: holderDid }), + issuer: new W3cIssuer({ + id: issuerDidKey.did, + }), + // NOTE: credentialSubject will be set at lower level as well, but we can also set it here + // FIXME: we should also set cnf like we set credentialSubject.id + credentialSubject: new W3cCredentialSubject({ + id: parseDid(holderBinding.didUrl).did, + }), issuanceDate: w3cDate(Date.now()), - }) + }), + verificationMethod: `${issuerDidKey.did}#${issuerDidKey.key.fingerprint}`, } + } + + if (credentialSupported.id === openBadgeCredential.id) { + assertDidBasedHolderBinding(holderBinding) - if (credentialRequest.format === 'jwt_vc_json' && credentialRequest.types.includes('OpenBadgeCredential')) { - return new W3cCredential({ + return { + credential: new W3cCredential({ type: openBadgeCredential.types, - issuer: new W3cIssuer({ id: this.did }), - credentialSubject: new W3cCredentialSubject({ id: holderDid }), + issuer: new W3cIssuer({ + id: issuerDidKey.did, + }), + credentialSubject: new W3cCredentialSubject({ + id: parseDid(holderBinding.didUrl).did, + }), issuanceDate: w3cDate(Date.now()), - }) + }), + verificationMethod: `${issuerDidKey.did}#${issuerDidKey.key.fingerprint}`, } + } - if ( - credentialRequest.format === 'vc+sd-jwt' && - credentialRequest.credential_definition.vct === 'UniversityDegreeCredential' - ) { - return new SdJwtCredential({ - payload: { type: 'UniversityDegreeCredential', university: 'innsbruck', degree: 'bachelor' }, - holderDidUrl, - issuerDidUrl: this.kid, - disclosureFrame: { university: true, degree: true }, - }) + if (credentialSupported.id === universityDegreeCredentialSdJwt.id) { + return { + payload: { vct: universityDegreeCredentialSdJwt.vct, university: 'innsbruck', degree: 'bachelor' }, + holder: holderBinding, + issuer: { + method: 'did', + didUrl: `${issuerDidKey.did}#${issuerDidKey.key.fingerprint}`, + }, + disclosureFrame: { university: true, degree: true }, } - - throw new Error('Invalid request') } + + throw new Error('Invalid request') + } +} + +export class Issuer extends BaseAgent<{ + sdJwtVc: SdJwtVcModule + askar: AskarModule + openId4VcIssuer: OpenId4VcIssuerModule +}> { + public issuerRecord!: OpenId4VcIssuerRecord + + public constructor(port: number, name: string) { + const openId4VciRouter = Router() + + super({ + port, + name, + modules: { + sdJwtVc: new SdJwtVcModule(), + askar: new AskarModule({ ariesAskar }), + openId4VcIssuer: new OpenId4VcIssuerModule({ + baseUrl: 'http://localhost:2000/oid4vci', + router: openId4VciRouter, + endpoints: { + credential: { + credentialRequestToCredentialMapper: (...args) => + getCredentialRequestToCredentialMapper({ issuerDidKey: this.didKey })(...args), + }, + }, + }), + } as const, + }) + + this.app.use('/oid4vci', openId4VciRouter) + } + + public static async build(): Promise { + const issuer = new Issuer(2000, 'OpenId4VcIssuer ' + Math.random().toString()) + await issuer.initializeAgent('96213c3d7fc8d4d6754c7a0fd969598f') + issuer.issuerRecord = await issuer.agent.modules.openId4VcIssuer.createIssuer({ + credentialsSupported, + }) + + return issuer } - public async createCredentialOffer(offeredCredentials: OfferedCredential[]) { - const { credentialOfferRequest } = await this.agent.modules.openId4VcIssuer.createCredentialOfferAndRequest( + public async createCredentialOffer(offeredCredentials: string[]) { + const { credentialOfferUri } = await this.agent.modules.openId4VcIssuer.createCredentialOffer({ + issuerId: this.issuerRecord.id, offeredCredentials, - { - scheme: 'openid-credential-offer', - preAuthorizedCodeFlowConfig: { userPinRequired: false }, - } - ) + scheme: 'openid-credential-offer', + preAuthorizedCodeFlowConfig: { userPinRequired: false }, + }) - return credentialOfferRequest + return credentialOfferUri } public async exit() { @@ -151,3 +173,9 @@ export class Issuer extends BaseAgent> await this.agent.shutdown() } } + +function assertDidBasedHolderBinding( + holderBinding: CredentialHolderBinding +): asserts holderBinding is CredentialHolderDidBinding { + throw new AriesFrameworkError('Only did based holder bindings supported for this credential type') +} diff --git a/demo-openid/src/IssuerInquirer.ts b/demo-openid/src/IssuerInquirer.ts index a4167cfee2..dca38ed86a 100644 --- a/demo-openid/src/IssuerInquirer.ts +++ b/demo-openid/src/IssuerInquirer.ts @@ -31,7 +31,6 @@ export class IssuerInquirer extends BaseInquirer { public static async build(): Promise { const issuer = await Issuer.build() - await issuer.configureRouter() return new IssuerInquirer(issuer) } @@ -60,9 +59,9 @@ export class IssuerInquirer extends BaseInquirer { const choice = await prompt([this.inquireOptions(credentialsSupported.map((credential) => credential.id))]) const offeredCredential = credentialsSupported.find((credential) => credential.id === choice.options) if (!offeredCredential) throw new Error(`No credential of type ${choice.options} found, that can be offered.`) - const offerRequest = await this.issuer.createCredentialOffer([offeredCredential]) + const offerRequest = await this.issuer.createCredentialOffer([offeredCredential.id]) - console.log(purpleText(`credential offer request: '${offerRequest}'`)) + console.log(purpleText(`credential offer: '${offerRequest}'`)) } public async exit() { diff --git a/package.json b/package.json index 35a2e9366d..13d304f29e 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,9 @@ "ws": "^8.13.0" }, "resolutions": { - "@types/node": "18.18.8" + "@types/node": "18.18.8", + "@sphereon/ssi-types": "^0.17.6-unstable.69", + "@sphereon/pex-models": "^2.1.2" }, "engines": { "node": ">=18" diff --git a/packages/core/src/crypto/JwsService.ts b/packages/core/src/crypto/JwsService.ts index 8e68c997f7..e9f76847ca 100644 --- a/packages/core/src/crypto/JwsService.ts +++ b/packages/core/src/crypto/JwsService.ts @@ -1,4 +1,10 @@ -import type { Jws, JwsDetachedFormat, JwsGeneralFormat, JwsProtectedHeaderOptions } from './JwsTypes' +import type { + Jws, + JwsDetachedFormat, + JwsFlattenedFormat, + JwsGeneralFormat, + JwsProtectedHeaderOptions, +} from './JwsTypes' import type { Key } from './Key' import type { Jwk } from './jose/jwk' import type { JwkJson } from './jose/jwk/Jwk' @@ -119,6 +125,11 @@ export class JwsService { throw new AriesFrameworkError('Unable to verify JWS, no signatures present in JWS.') } + const jwsFlattened = { + signatures, + payload, + } satisfies JwsFlattenedFormat + const signerKeys: Key[] = [] for (const jws of signatures) { const protectedJson = JsonEncoder.fromBase64(jws.protected) @@ -159,6 +170,7 @@ export class JwsService { return { isValid: false, signerKeys: [], + jws: jwsFlattened, } } } catch (error) { @@ -168,6 +180,7 @@ export class JwsService { return { isValid: false, signerKeys: [], + jws: jwsFlattened, } } @@ -175,7 +188,7 @@ export class JwsService { } } - return { isValid: true, signerKeys } + return { isValid: true, signerKeys, jws: jwsFlattened } } private buildProtected(options: JwsProtectedHeaderOptions) { @@ -268,10 +281,12 @@ export interface VerifyJwsOptions { export type JwsJwkResolver = (options: { jws: JwsDetachedFormat payload: string - protectedHeader: { alg: string; [key: string]: unknown } + protectedHeader: { alg: string; jwk?: string; kid?: string; [key: string]: unknown } }) => Promise | Jwk export interface VerifyJwsResult { isValid: boolean signerKeys: Key[] + + jws: JwsFlattenedFormat } diff --git a/packages/core/src/crypto/jose/jwt/Jwt.ts b/packages/core/src/crypto/jose/jwt/Jwt.ts index cca3c3a454..eb73ca05dc 100644 --- a/packages/core/src/crypto/jose/jwt/Jwt.ts +++ b/packages/core/src/crypto/jose/jwt/Jwt.ts @@ -10,7 +10,7 @@ import { JwtPayload } from './JwtPayload' interface JwtHeader { alg: string kid?: string - jwk: JwkJson + jwk?: JwkJson [key: string]: unknown } diff --git a/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiableCredential.ts b/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiableCredential.ts index 2fad970565..740c639472 100644 --- a/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiableCredential.ts +++ b/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiableCredential.ts @@ -41,6 +41,10 @@ export class W3cJsonLdVerifiableCredential extends W3cCredential { return JsonTransformer.toJSON(this) } + public static fromJson(json: Record) { + return JsonTransformer.fromJSON(json, W3cJsonLdVerifiableCredential) + } + /** * The {@link ClaimFormat} of the credential. For JSON-LD credentials this is always `ldp_vc`. */ diff --git a/packages/core/src/utils/path.ts b/packages/core/src/utils/path.ts index 0f9196cdc1..605a712d93 100644 --- a/packages/core/src/utils/path.ts +++ b/packages/core/src/utils/path.ts @@ -14,11 +14,21 @@ export function getDirFromFilePath(path: string) { * @param parts the parts to combine * @returns the combined url */ -export function joinUriParts(parts: string[]) { - let combined = '' +export function joinUriParts(base: string, parts: string[]) { + if (parts.length === 0) return base + + // take base without trailing / + let combined = base.endsWith('/') ? base.slice(0, base.length - 1) : base for (const part of parts) { - combined += part.endsWith('/') ? part : `${part}/` + // Remove leading and trailing / + let strippedPart = part.startsWith('/') ? part.slice(1) : part + strippedPart = strippedPart.endsWith('/') ? strippedPart.slice(0, strippedPart.length - 1) : strippedPart + + // Don't want to add if empty + if (strippedPart === '') continue + + combined += `/${strippedPart}` } return combined diff --git a/packages/openid4vc/package.json b/packages/openid4vc/package.json index 1ff27084e2..a128b55880 100644 --- a/packages/openid4vc/package.json +++ b/packages/openid4vc/package.json @@ -26,14 +26,11 @@ "dependencies": { "@aries-framework/askar": "^0.4.2", "@aries-framework/core": "0.4.2", - "@sphereon/ssi-types": "^0.17.5", - "@sphereon/oid4vci-client": "file:./../../../Sphereon/sphereon-oidvci-client-0.8.1", - "@sphereon/oid4vci-common": "file:./../../../Sphereon/sphereon-oid4vci-common-0.8.1", - "@sphereon/oid4vci-issuer-server": "file:./../../../Sphereon/sphereon-oid4vci-issuer-server-0.8.1", - "@sphereon/oid4vci-issuer": "file:./../../../Sphereon/sphereon-oid4vci-issuer-0.8.1", - "@sphereon/did-auth-siop": "^0.5.0-unstable.8", - "@sphereon/pex": "2.2.0", - "@sphereon/pex-models": "^2.1.1", + "@sphereon/ssi-types": "^0.17.6-unstable.69", + "@sphereon/oid4vci-client": "0.8.2-next.26", + "@sphereon/oid4vci-common": "0.8.2-next.26", + "@sphereon/oid4vci-issuer": "0.8.2-next.26", + "@sphereon/pex-models": "^2.1.2", "body-parser": "^1.20.2", "jsonpath": "^1.1.1" }, diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts index a0efc1862f..79fc119e3a 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts @@ -6,14 +6,15 @@ import type { AcceptCredentialOfferOptions, CredentialOfferPayloadV1_0_11, } from './reception' -import type { VerificationMethod, W3cCredentialRecord } from '@aries-framework/core' -import type { SdJwtVcRecord } from '@aries-framework/sd-jwt-vc' +import type { VerificationMethod } from '@aries-framework/core' import { injectable, AgentContext } from '@aries-framework/core' import { OpenId4VpHolderService } from './presentation' import { OpenId4VciHolderService } from './reception' +// FIXME: the holder API is not really consistent with the issuer API +// FIXME: it's not immediately clear which methods are for receiving vc proving /** * @public */ @@ -130,7 +131,7 @@ export class OpenId4VcHolderApi { public async acceptCredentialOfferUsingPreAuthorizedCode( resolvedCredentialOffer: ResolvedCredentialOffer, acceptCredentialOfferOptions: AcceptCredentialOfferOptions - ): Promise<(W3cCredentialRecord | SdJwtVcRecord)[]> { + ) { return this.openId4VciHolderService.acceptCredentialOffer(this.agentContext, { resolvedCredentialOffer, acceptCredentialOfferOptions, @@ -150,7 +151,7 @@ export class OpenId4VcHolderApi { resolvedAuthorizationRequest: ResolvedAuthorizationRequest, code: string, acceptCredentialOfferOptions: AcceptCredentialOfferOptions - ): Promise<(W3cCredentialRecord | SdJwtVcRecord)[]> { + ) { return this.openId4VciHolderService.acceptCredentialOffer(this.agentContext, { resolvedCredentialOffer, resolvedAuthorizationRequestWithCode: { ...resolvedAuthorizationRequest, code }, diff --git a/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderService.ts b/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderService.ts index 11c84a529f..53b90eba48 100644 --- a/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderService.ts @@ -1,12 +1,6 @@ import type { OfferedCredentialWithMetadata } from './utils/IssuerMetadataUtils' -import type { - AgentContext, - JwaSignatureAlgorithm, - VerificationMethod, - W3cVerifiableCredential, - W3cVerifyCredentialResult, -} from '@aries-framework/core' -import type { SdJwtVcRecord, SdJwtVcModule } from '@aries-framework/sd-jwt-vc' +import type { AgentContext, JwaSignatureAlgorithm, W3cVerifiableCredential, Key, JwkJson } from '@aries-framework/core' +import type { SdJwtVcModule, SdJwtVc } from '@aries-framework/sd-jwt-vc' import type { AccessTokenResponse, CredentialOfferPayloadV1_0_11, @@ -20,16 +14,16 @@ import type { } from '@sphereon/oid4vci-common' import { + getJwkFromJson, + DidsApi, AriesFrameworkError, Hasher, InjectionSymbols, JsonEncoder, - JsonTransformer, JwsService, Logger, SignatureSuiteRegistry, TypedArrayEncoder, - W3cCredentialRecord, W3cCredentialService, W3cJsonLdVerifiableCredential, W3cJwtVerifiableCredential, @@ -65,7 +59,7 @@ import { type AuthCodeFlowOptions, type AcceptCredentialOfferOptions, type ProofOfPossessionRequirements, - type ProofOfPossessionVerificationMethodResolver, + type CredentialBindingResolver, type ResolvedCredentialOffer, type ResolvedAuthorizationRequest, type ResolvedAuthorizationRequestWithCode, @@ -82,6 +76,7 @@ import { OfferedCredentialType, } from './utils/IssuerMetadataUtils' +// FIXME: remove support for draft 8 function getV8CredentialType(offeredCredentialWithMetadata: OfferedCredentialWithMetadata, version: OpenId4VCIVersion) { if (offeredCredentialWithMetadata.offerType === OfferedCredentialType.InlineCredentialOffer) { throw new AriesFrameworkError(`Inline credential offers not supported for version < 11`) @@ -263,15 +258,16 @@ export class OpenId4VciHolderService { return { type, format, locations, credential_definition } } else if (format === OpenIdCredentialFormatProfile.SdJwtVc) { - const credential_definition = { + return { + type, + format, + locations, vct: types[0], claims: offerType === OfferedCredentialType.InlineCredentialOffer - ? credentialWithMetadata.credentialOffer.credential_definition.claims - : credentialWithMetadata.credentialSupported.credential_definition.claims, + ? credentialWithMetadata.credentialOffer.claims + : credentialWithMetadata.credentialSupported.claims, } - - return { type, format, locations, credential_definition } } else { throw new AriesFrameworkError(`Cannot create authorization_details. Unsupported credential format '${format}'.`) } @@ -344,7 +340,7 @@ export class OpenId4VciHolderService { const { resolvedCredentialOffer, acceptCredentialOfferOptions, resolvedAuthorizationRequestWithCode } = options const { credentialOfferPayload, metadata: _metadata, version } = resolvedCredentialOffer - const { credentialsToRequest, userPin, proofOfPossessionVerificationMethodResolver, verifyCredentialStatus } = + const { credentialsToRequest, userPin, credentialBindingResolver, verifyCredentialStatus } = acceptCredentialOfferOptions if (credentialsToRequest?.length === 0) { @@ -426,28 +422,32 @@ export class OpenId4VciHolderService { return credentialToRequest }) - const receivedCredentials: (W3cCredentialRecord | SdJwtVcRecord)[] = [] + const receivedCredentials: (W3cVerifiableCredential | SdJwtVc)[] = [] let newCNonce: string | undefined for (const credentialWithMetadata of credentialsToRequestWithMetadata ?? offeredCredentialsWithMetadata) { // Get all options for the credential request (such as which kid to use, the signature algorithm, etc) - const { verificationMethod, signatureAlgorithm } = await this.getCredentialRequestOptions(agentContext, { + const { credentialBinding, signatureAlgorithm } = await this.getCredentialRequestOptions(agentContext, { possibleProofOfPossessionSignatureAlgorithms: possibleProofOfPossessionSigAlgs, offeredCredentialWithMetadata: credentialWithMetadata, - proofOfPossessionVerificationMethodResolver, + credentialBindingResolver, }) // Create the proof of possession const proofOfPossessionBuilder = ProofOfPossessionBuilder.fromAccessTokenResponse({ accessTokenResponse: accessToken, - callbacks: { signCallback: this.signCallback(agentContext, verificationMethod) }, + callbacks: { signCallback: this.proofOfPossessionSignCallback(agentContext) }, version, }) .withEndpointMetadata(metadata) .withAlg(signatureAlgorithm) - .withClientId(verificationMethod.controller) - .withKid(verificationMethod.id) + + if (credentialBinding.method === 'did') { + proofOfPossessionBuilder.withClientId(parseDid(credentialBinding.didUrl).did).withKid(credentialBinding.didUrl) + } else if (credentialBinding.method === 'jwk') { + proofOfPossessionBuilder.withJWK(credentialBinding.jwk.toJson()) + } if (newCNonce) proofOfPossessionBuilder.withAccessTokenNonce(newCNonce) @@ -482,14 +482,13 @@ export class OpenId4VciHolderService { newCNonce = credentialResponse.successBody?.c_nonce - // Create credential record, but we don't store it yet (only after the user has accepted the credential) - const credentialRecord = await this.handleCredentialResponse(agentContext, credentialResponse, { + // Create credential, but we don't store it yet (only after the user has accepted the credential) + const credential = await this.handleCredentialResponse(agentContext, credentialResponse, { verifyCredentialStatus: verifyCredentialStatus ?? false, - holderDidUrl: verificationMethod.id, }) - this.logger.debug('Full credential', credentialRecord) - receivedCredentials.push(credentialRecord) + this.logger.debug('Full credential', credential) + receivedCredentials.push(credential) } return receivedCredentials @@ -504,18 +503,16 @@ export class OpenId4VciHolderService { private async getCredentialRequestOptions( agentContext: AgentContext, options: { - proofOfPossessionVerificationMethodResolver: ProofOfPossessionVerificationMethodResolver + credentialBindingResolver: CredentialBindingResolver possibleProofOfPossessionSignatureAlgorithms: JwaSignatureAlgorithm[] offeredCredentialWithMetadata: OfferedCredentialWithMetadata } ) { - const { signatureAlgorithm, supportedDidMethods, supportsAllDidMethods } = this.getProofOfPossessionRequirements( - agentContext, - { + const { signatureAlgorithm, supportedDidMethods, supportsAllDidMethods, supportsJwk } = + this.getProofOfPossessionRequirements(agentContext, { credentialsToRequest: options.offeredCredentialWithMetadata, possibleProofOfPossessionSignatureAlgorithms: options.possibleProofOfPossessionSignatureAlgorithms, - } - ) + }) const JwkClass = getJwkClassFromJwaSignatureAlgorithm(signatureAlgorithm) if (!JwkClass) { @@ -528,45 +525,51 @@ export class OpenId4VciHolderService { const format = options.offeredCredentialWithMetadata.format as SupportedCredentialFormats - // Now we need to determine the did method and alg based on the cryptographic suite - const verificationMethod = await options.proofOfPossessionVerificationMethodResolver({ + // Now we need to determine how the credential will be bound to us + const credentialBinding = await options.credentialBindingResolver({ credentialFormat: format, - proofOfPossessionSignatureAlgorithm: signatureAlgorithm, + signatureAlgorithm, supportedVerificationMethods, keyType: JwkClass.keyType, - supportedCredentialId: !( - options.offeredCredentialWithMetadata.offerType === OfferedCredentialType.InlineCredentialOffer - ) - ? options.offeredCredentialWithMetadata.credentialSupported.id - : undefined, + supportedCredentialId: + options.offeredCredentialWithMetadata.offerType === OfferedCredentialType.CredentialSupported + ? options.offeredCredentialWithMetadata.credentialSupported.id + : undefined, supportsAllDidMethods, supportedDidMethods, + supportsJwk, }) - // Make sure the verification method uses a supported did method + // Make sure the issuer of proof of possession is valid according to openid issuer metadata if ( + credentialBinding.method === 'did' && !supportsAllDidMethods && // If supportedDidMethods is undefined, it means the issuer didn't include the binding methods in the metadata // The user can still select a verification method, but we can't validate it supportedDidMethods !== undefined && - !supportedDidMethods.find((supportedDidMethod) => verificationMethod.id.startsWith(supportedDidMethod)) + !supportedDidMethods.find((supportedDidMethod) => credentialBinding.didUrl.startsWith(supportedDidMethod)) ) { - const { method } = parseDid(verificationMethod.id) + const { method } = parseDid(credentialBinding.didUrl) const supportedDidMethodsString = supportedDidMethods.join(', ') throw new AriesFrameworkError( - `Verification method uses did method '${method}', but issuer only supports '${supportedDidMethodsString}'` + `Resolved credential binding for proof of possession uses did method '${method}', but issuer only supports '${supportedDidMethodsString}'` ) - } - - // Make sure the verification method uses a supported verification method type - if (!supportedVerificationMethods.includes(verificationMethod.type)) { - const supportedVerificationMethodsString = supportedVerificationMethods.join(', ') + } else if (credentialBinding.method === 'jwk' && !supportsJwk) { throw new AriesFrameworkError( - `Verification method uses verification method type '${verificationMethod.type}', but only '${supportedVerificationMethodsString}' verification methods are supported for key type '${JwkClass.keyType}'` + `Resolved credential binding for proof of possession uses jwk, but openid issuer does not support 'jwk' cryptographic binding method` ) } - return { verificationMethod, signatureAlgorithm } + // FIXME: we don't have the verification method here + // Make sure the verification method uses a supported verification method type + // if (!supportedVerificationMethods.includes(verificationMethod.type)) { + // const supportedVerificationMethodsString = supportedVerificationMethods.join(', ') + // throw new AriesFrameworkError( + // `Verification method uses verification method type '${verificationMethod.type}', but only '${supportedVerificationMethodsString}' verification methods are supported for key type '${JwkClass.keyType}'` + // ) + // } + + return { credentialBinding, signatureAlgorithm } } /** @@ -648,27 +651,29 @@ export class OpenId4VciHolderService { const supportsAllDidMethods = issuerSupportedBindingMethods?.includes('did') ?? false const supportedDidMethods = issuerSupportedBindingMethods?.filter((method) => method.startsWith('did:')) + const supportsJwk = issuerSupportedBindingMethods?.includes('jwk') ?? false return { signatureAlgorithm, supportedDidMethods, supportsAllDidMethods, + supportsJwk, } } + private async handleCredentialResponse( agentContext: AgentContext, credentialResponse: OpenIDResponse, - options: { verifyCredentialStatus: boolean; holderDidUrl: string } - ): Promise { - const { verifyCredentialStatus, holderDidUrl } = options + options: { verifyCredentialStatus: boolean } + ): Promise { + const { verifyCredentialStatus } = options this.logger.debug('Credential request response', credentialResponse) - if (!credentialResponse.successBody) { + if (!credentialResponse.successBody || !credentialResponse.successBody.credential) { throw new AriesFrameworkError('Did not receive a successful credential response.') } const format = getUniformFormat(credentialResponse.successBody.format) - if (format === OpenIdCredentialFormatProfile.SdJwtVc) { if (typeof credentialResponse.successBody.credential !== 'string') throw new AriesFrameworkError( @@ -679,65 +684,105 @@ export class OpenId4VciHolderService { const sdJwtVcApi = getApiForModuleByName(agentContext, 'SdJwtVcModule') if (!sdJwtVcApi) throw new AriesFrameworkError(`Could not find the SdJwtVcApi`) - const sdJwtVcRecord = await sdJwtVcApi.fromSerializedJwt(credentialResponse.successBody.credential, { - holderDidUrl, + const { verification, sdJwtVc } = await sdJwtVcApi.verify({ + compactSdJwtVc: credentialResponse.successBody.credential, }) - return sdJwtVcRecord - } + if (!verification.isValid) { + agentContext.config.logger.error('Failed to validate credential', { verification }) + throw new AriesFrameworkError( + `Failed to validate sd-jwt-vc credential. Results = ${JSON.stringify(verification)}` + ) + } - let credential: W3cVerifiableCredential - let result: W3cVerifyCredentialResult - if (format === OpenIdCredentialFormatProfile.LdpVc || format === OpenIdCredentialFormatProfile.JwtVcJsonLd) { - // validate json-ld credentials - credential = JsonTransformer.fromJSON(credentialResponse.successBody.credential, W3cJsonLdVerifiableCredential) - result = await this.w3cCredentialService.verifyCredential(agentContext, { credential, verifyCredentialStatus }) - } else if (format === OpenIdCredentialFormatProfile.JwtVcJson) { - // validate jwt credentials - credential = W3cJwtVerifiableCredential.fromSerializedJwt(credentialResponse.successBody.credential as string) - result = await this.w3cCredentialService.verifyCredential(agentContext, { credential, verifyCredentialStatus }) - } else { - throw new AriesFrameworkError(`Unsupported credential format ${credentialResponse.successBody.format}`) - } + return sdJwtVc + } else if ( + format === OpenIdCredentialFormatProfile.JwtVcJson || + format === OpenIdCredentialFormatProfile.JwtVcJsonLd + ) { + const credential = W3cJwtVerifiableCredential.fromSerializedJwt( + credentialResponse.successBody.credential as string + ) + const result = await this.w3cCredentialService.verifyCredential(agentContext, { + credential, + verifyCredentialStatus, + }) + if (!result.isValid) { + agentContext.config.logger.error('Failed to validate credential', { result }) + throw new AriesFrameworkError(`Failed to validate credential, error = ${result.error?.message ?? 'Unknown'}`) + } + + return credential + } else if (format === OpenIdCredentialFormatProfile.LdpVc) { + const credential = W3cJsonLdVerifiableCredential.fromJson( + credentialResponse.successBody.credential as Record + ) + const result = await this.w3cCredentialService.verifyCredential(agentContext, { + credential, + verifyCredentialStatus, + }) + if (!result.isValid) { + agentContext.config.logger.error('Failed to validate credential', { result }) + throw new AriesFrameworkError(`Failed to validate credential, error = ${result.error?.message ?? 'Unknown'}`) + } - if (!result.isValid) { - agentContext.config.logger.error('Failed to validate credential', { result }) - throw new AriesFrameworkError(`Failed to validate credential, error = ${result.error?.message ?? 'Unknown'}`) + return credential } - return new W3cCredentialRecord({ credential, tags: { expandedTypes: [] } }) + throw new AriesFrameworkError(`Unsupported credential format ${credentialResponse.successBody.format}`) } - private signCallback(agentContext: AgentContext, verificationMethod: VerificationMethod) { + private proofOfPossessionSignCallback(agentContext: AgentContext) { return async (jwt: Jwt, kid?: string) => { if (!jwt.header) throw new AriesFrameworkError('No header present on JWT') if (!jwt.payload) throw new AriesFrameworkError('No payload present on JWT') - if (!kid) throw new AriesFrameworkError('No KID is present in the callback') + if (kid && jwt.header.jwk) { + throw new AriesFrameworkError('Both KID and JWK are present in the callback. Only one can be present') + } + + let key: Key - // We have determined the verification method before and already passed that when creating the callback, - // however we just want to make sure that the kid matches the verification method id - if (verificationMethod.id !== kid) { - throw new AriesFrameworkError(`kid ${kid} does not match verification method id ${verificationMethod.id}`) + if (kid) { + if (!kid.startsWith('did:')) { + throw new AriesFrameworkError(`kid '${kid}' is not a DID. Only dids are supported for kid`) + } else if (!kid.includes('#')) { + throw new AriesFrameworkError( + `kid '${kid}' does not contain a fragment. kid MUST point to a specific key in the did document.` + ) + } + + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const didDocument = await didsApi.resolveDidDocument(kid) + const verificationMethod = didDocument.dereferenceKey(kid, ['authentication']) + + key = getKeyFromVerificationMethod(verificationMethod) + } else if (jwt.header.jwk) { + key = getJwkFromJson(jwt.header.jwk as JwkJson).key + } else { + throw new AriesFrameworkError('No KID or JWK is present in the callback') } - const key = getKeyFromVerificationMethod(verificationMethod) const jwk = getJwkFromKey(key) if (!jwk.supportsSignatureAlgorithm(jwt.header.alg)) { throw new AriesFrameworkError( - `kid ${kid} refers to a key of type '${jwk.keyType}', which does not support the JWS signature alg '${jwt.header.alg}'` + `key type '${jwk.keyType}', does not support the JWS signature alg '${jwt.header.alg}'` ) } // We don't support these properties, remove them, so we can pass all other header properties to the JWS service - if (jwt.header.x5c || jwt.header.jwk) throw new AriesFrameworkError('x5c and jwk are not supported') + if (jwt.header.x5c) throw new AriesFrameworkError('x5c is not supported') // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { x5c: _x5c, jwk: _jwk, ...supportedHeaderOptions } = jwt.header + const { x5c: _x5c, ...supportedHeaderOptions } = jwt.header const jws = await this.jwsService.createJwsCompact(agentContext, { key, payload: JsonEncoder.toBuffer(jwt.payload), - protectedHeaderOptions: supportedHeaderOptions, + protectedHeaderOptions: { + ...supportedHeaderOptions, + // only pass jwk if it was present in the header + jwk: jwt.header.jwk ? jwk : undefined, + }, }) return jws diff --git a/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderServiceOptions.ts b/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderServiceOptions.ts index 80aeb69de5..df88d6593d 100644 --- a/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderServiceOptions.ts @@ -1,5 +1,6 @@ import type { OfferedCredentialWithMetadata } from './utils/IssuerMetadataUtils' -import type { JwaSignatureAlgorithm, KeyType, VerificationMethod } from '@aries-framework/core' +import type { CredentialHolderBinding } from '../../shared' +import type { JwaSignatureAlgorithm, KeyType } from '@aries-framework/core' import type { CredentialOfferPayloadV1_0_11, EndpointMetadataResult, @@ -71,15 +72,19 @@ export interface AcceptCredentialOfferOptions { allowedProofOfPossessionSignatureAlgorithms?: JwaSignatureAlgorithm[] /** - * A function that should resolve a verification method based on the options passed. + * A function that should resolve key material for binding the to-be-issued credential + * to the holder based on the options passed. This key material will be used for signing + * the proof of possession included in the credential request. + * * This method will be called once for each of the credentials that are included * in the credential offer. * * Based on the credential format, JWA signature algorithm, verification method types - * and did methods, the resolver must return a verification method that will be used + * and binding methods (did methods, jwk), the resolver must return an object + * conformant to the `CredentialHolderBinding` interface, which will be used * for the proof of possession signature. */ - proofOfPossessionVerificationMethodResolver: ProofOfPossessionVerificationMethodResolver + credentialBindingResolver: CredentialBindingResolver } /** @@ -92,7 +97,7 @@ export interface AuthCodeFlowOptions { scope?: string[] } -export interface ProofOfPossessionVerificationMethodResolverOptions { +export interface CredentialBindingOptions { /** * The credential format that will be requested from the issuer. * E.g. `jwt_vc` or `ldp_vc`. @@ -104,7 +109,7 @@ export interface ProofOfPossessionVerificationMethodResolverOptions { * This is based on the `allowedProofOfPossessionSignatureAlgorithms` passed * to the request credential method, and the supported signature algorithms. */ - proofOfPossessionSignatureAlgorithm: JwaSignatureAlgorithm + signatureAlgorithm: JwaSignatureAlgorithm /** * This is a list of verification methods types that are supported @@ -156,6 +161,12 @@ export interface ProofOfPossessionVerificationMethodResolverOptions { * is true, the value of this property MUST be ignored. */ supportedDidMethods?: string[] + + /** + * Whether the issuer supports the `jwk` cryptographic binding method, + * indicating they support proof of possession signatures bound to a jwk. + */ + supportsJwk: boolean } /** @@ -163,9 +174,9 @@ export interface ProofOfPossessionVerificationMethodResolverOptions { * user of the framework and allows them to determine which verification method should be used * for the proof of possession signature. */ -export type ProofOfPossessionVerificationMethodResolver = ( - options: ProofOfPossessionVerificationMethodResolverOptions -) => Promise | VerificationMethod +export type CredentialBindingResolver = ( + options: CredentialBindingOptions +) => Promise | CredentialHolderBinding /** * @internal @@ -174,4 +185,5 @@ export interface ProofOfPossessionRequirements { signatureAlgorithm: JwaSignatureAlgorithm supportedDidMethods?: string[] supportsAllDidMethods: boolean + supportsJwk: boolean } diff --git a/packages/openid4vc/src/openid4vc-holder/reception/utils/IssuerMetadataUtils.ts b/packages/openid4vc/src/openid4vc-holder/reception/utils/IssuerMetadataUtils.ts index dd2b4f2dc4..511ac4fab0 100644 --- a/packages/openid4vc/src/openid4vc-holder/reception/utils/IssuerMetadataUtils.ts +++ b/packages/openid4vc/src/openid4vc-holder/reception/utils/IssuerMetadataUtils.ts @@ -1,7 +1,5 @@ import type { AuthorizationDetails, - CommonCredentialOfferFormat, - CommonCredentialSupported, CredentialIssuerMetadata, CredentialOfferFormat, CredentialOfferFormatJwtVcJson, @@ -35,44 +33,50 @@ export enum OfferedCredentialType { InlineCredentialOffer = 'InlineCredentialOffer', } -export type OfferedCredentialWithMetadata = +export type InlineOfferedCredentialWithMetadata = | { - offerType: OfferedCredentialType.CredentialSupported + offerType: OfferedCredentialType.InlineCredentialOffer format: OpenIdCredentialFormatProfile.JwtVcJson - credentialSupported: CommonCredentialSupported & CredentialSupportedJwtVcJson + credentialOffer: CredentialOfferFormatJwtVcJson types: string[] } | { - offerType: OfferedCredentialType.CredentialSupported + offerType: OfferedCredentialType.InlineCredentialOffer format: OpenIdCredentialFormatProfile.JwtVcJsonLd | OpenIdCredentialFormatProfile.LdpVc - credentialSupported: CommonCredentialSupported & CredentialSupportedJwtVcJsonLdAndLdpVc + credentialOffer: CredentialOfferFormatJwtVcJsonLdAndLdpVc types: string[] } | { - offerType: OfferedCredentialType.CredentialSupported + offerType: OfferedCredentialType.InlineCredentialOffer format: OpenIdCredentialFormatProfile.SdJwtVc - credentialSupported: CommonCredentialSupported & CredentialSupportedSdJwtVc + credentialOffer: CredentialOfferFormatSdJwtVc types: string[] } + +export type ReferencedOfferedCredentialWithMetadata = | { - offerType: OfferedCredentialType.InlineCredentialOffer + offerType: OfferedCredentialType.CredentialSupported format: OpenIdCredentialFormatProfile.JwtVcJson - credentialOffer: CommonCredentialOfferFormat & CredentialOfferFormatJwtVcJson + credentialSupported: CredentialSupportedJwtVcJson types: string[] } | { - offerType: OfferedCredentialType.InlineCredentialOffer + offerType: OfferedCredentialType.CredentialSupported format: OpenIdCredentialFormatProfile.JwtVcJsonLd | OpenIdCredentialFormatProfile.LdpVc - credentialOffer: CommonCredentialOfferFormat & CredentialOfferFormatJwtVcJsonLdAndLdpVc + credentialSupported: CredentialSupportedJwtVcJsonLdAndLdpVc types: string[] } | { - offerType: OfferedCredentialType.InlineCredentialOffer + offerType: OfferedCredentialType.CredentialSupported format: OpenIdCredentialFormatProfile.SdJwtVc - credentialOffer: CommonCredentialOfferFormat & CredentialOfferFormatSdJwtVc + credentialSupported: CredentialSupportedSdJwtVc types: string[] } +export type OfferedCredentialWithMetadata = + | ReferencedOfferedCredentialWithMetadata + | InlineOfferedCredentialWithMetadata + /** * Returns all entries from the credential offer with the associated metadata resolved. For inline entries, the offered credential object * is included directly. For 'id' entries, the associated `credentials_supported` object is resolved from the issuer metadata. @@ -110,7 +114,7 @@ export function getOfferedCredentialsWithMetadata( offerType: OfferedCredentialType.CredentialSupported, credentialSupported: foundSupportedCredential, format: OpenIdCredentialFormatProfile.SdJwtVc, - types: [foundSupportedCredential.credential_definition.vct], + types: [foundSupportedCredential.vct], }) } else { offeredCredentialsWithMetadata.push({ @@ -130,7 +134,7 @@ export function getOfferedCredentialsWithMetadata( } else if (offeredCredential.format === 'jwt_vc_json-ld' || offeredCredential.format === 'ldp_vc') { types = offeredCredential.credential_definition.types } else if (offeredCredential.format === 'vc+sd-jwt') { - types = [offeredCredential.credential_definition.vct] + types = [offeredCredential.vct] } else { throw new AriesFrameworkError(`Unknown format received ${JSON.stringify(offeredCredential.format)}`) } diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts index b65372616e..0d0c3945f1 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts @@ -1,7 +1,6 @@ import type { OpenId4VcIssuerModuleConfigOptions } from './OpenId4VcIssuerModuleConfig' import type { IssuanceRequest } from './router/requestContext' import type { AgentContext, DependencyManager, Module } from '@aries-framework/core' -import type { Router } from 'express' import { AgentConfig } from '@aries-framework/core' @@ -21,14 +20,9 @@ import { getAgentContextForIssuerId } from './router/requestContext' export class OpenId4VcIssuerModule implements Module { public readonly api = OpenId4VcIssuerApi public readonly config: OpenId4VcIssuerModuleConfig - public readonly router: Router public constructor(options: OpenId4VcIssuerModuleConfigOptions) { this.config = new OpenId4VcIssuerModuleConfig(options) - - // Initialize the router. The user still needs to register the router on their own express - // application. - this.router = importExpress().Router() } /** @@ -68,13 +62,14 @@ export class OpenId4VcIssuerModule implements Module { // We use separate context router and endpoint router. Context router handles the linking of the request // to a specific agent context. Endpoint router only knows about a single context const endpointRouter = Router() + const contextRouter = this.config.router // parse application/x-www-form-urlencoded - this.router.use(urlencoded({ extended: false })) + contextRouter.use(urlencoded({ extended: false })) // parse application/json - this.router.use(json()) + contextRouter.use(json()) - this.router.param('issuerId', async (req: IssuanceRequest, _res, next, issuerId: string) => { + contextRouter.param('issuerId', async (req: IssuanceRequest, _res, next, issuerId: string) => { if (!issuerId) { _res.status(404).send('Not found') } @@ -91,6 +86,12 @@ export class OpenId4VcIssuerModule implements Module { issuer, } } catch (error) { + agentContext?.config.logger.error( + 'Failed to correlate incoming oid4vci request to existing tenant and issuer', + { + error, + } + ) // If the opening failed await agentContext?.endSession() return _res.status(404).send('Not found') @@ -99,7 +100,7 @@ export class OpenId4VcIssuerModule implements Module { next() }) - this.router.use('/:issuerId', endpointRouter) + contextRouter.use('/:issuerId', endpointRouter) // Configure endpoints configureIssuerMetadataEndpoint(endpointRouter) @@ -107,7 +108,7 @@ export class OpenId4VcIssuerModule implements Module { configureCredentialEndpoint(endpointRouter, this.config.credentialEndpoint) // FIXME: Will this be called when an error occurs / 404 is returned earlier on? - this.router.use(async (req: IssuanceRequest, _res, next) => { + contextRouter.use(async (req: IssuanceRequest, _res, next) => { const { agentContext } = getRequestContext(req) await agentContext.endSession() next() diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts index 1017b9ee03..aebc8f9915 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts @@ -1,9 +1,12 @@ import type { AccessTokenEndpointConfig, CredentialEndpointConfig } from './OpenId4VcIssuerServiceOptions' import type { AgentContext } from '@aries-framework/core' import type { CNonceState, CredentialOfferSession, IStateManager, StateType, URIState } from '@sphereon/oid4vci-common' +import type { Router } from 'express' import { MemoryStates } from '@sphereon/oid4vci-issuer' +import { importExpress } from './router/express' + export type StateManagerFactory = () => IStateManager const DEFAULT_C_NONCE_EXPIRES_IN = 5 * 60 * 1000 // 5 minutes @@ -17,6 +20,15 @@ export interface OpenId4VcIssuerModuleConfigOptions { */ baseUrl: string + /** + * Express router on which the openid4vci endpoints will be registered. If + * no router is provided, a new one will be created. + * + * NOTE: you must manually register the router on your express app and + * expose this on a public url that is reachable when `baseUrl` is called. + */ + router?: Router + endpoints: { // metadata endpoint does not have a config // metadata?: MetadataEndpointConfig @@ -40,12 +52,15 @@ export class OpenId4VcIssuerModuleConfig { private uriStateManagerMap: Map> private credentialOfferSessionManagerMap: Map> private cNonceStateManagerMap: Map> + public readonly router: Router public constructor(options: OpenId4VcIssuerModuleConfigOptions) { this.uriStateManagerMap = new Map() this.credentialOfferSessionManagerMap = new Map() this.cNonceStateManagerMap = new Map() this.options = options + + this.router = options.router ?? importExpress().Router() } public get baseUrl() { diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts index e599cb61f5..527a6af405 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts @@ -3,15 +3,17 @@ import type { CreateCredentialOfferOptions, CreateCredentialResponseOptions, CreateIssuerOptions, - CredentialHolderBinding, CredentialOffer, IssuerMetadata, OpenId4VciSignCredential, + OpenId4VciSignSdJwtCredential, + OpenId4VciSignW3cCredential, PreAuthorizedCodeFlowConfig, } from './OpenId4VcIssuerServiceOptions' import type { ReferencedOfferedCredentialWithMetadata } from '../openid4vc-holder/reception/utils/IssuerMetadataUtils' -import type { AgentContext, DidDocument, Jwk, W3cSignCredentialOptions } from '@aries-framework/core' -import type { SdJwtVcModule, SdJwtVcSignOptions } from '@aries-framework/sd-jwt-vc' +import type { CredentialHolderBinding } from '../shared' +import type { AgentContext, DidDocument } from '@aries-framework/core' +import type { SdJwtVcModule } from '@aries-framework/sd-jwt-vc' import type { CredentialOfferPayloadV1_0_11, CredentialRequestV1_0_11, @@ -28,6 +30,8 @@ import type { import type { ICredential } from '@sphereon/ssi-types' import { + ClaimFormat, + JsonEncoder, getJwkFromJson, KeyType, utils, @@ -54,13 +58,14 @@ import { getOfferedCredentialsWithMetadata, } from '../openid4vc-holder/reception/utils/IssuerMetadataUtils' import { getSphereonW3cVerifiableCredential } from '../shared/transform' +import { getProofTypeFromVerificationMethod } from '../shared/utils' import { OpenId4VcIssuerModuleConfig } from './OpenId4VcIssuerModuleConfig' import { OpenId4VcIssuerRecord } from './repository/OpenId4VcIssuerRecord' import { OpenId4VcIssuerRepository } from './repository/OpenId4VcIssuerRepository' import { storeIssuerIdForContextCorrelationId } from './router/requestContext' -const W3cOpenId4VcFormats = [ +const w3cOpenId4VcFormats = [ OpenIdCredentialFormatProfile.JwtVcJson, OpenIdCredentialFormatProfile.JwtVcJsonLd, OpenIdCredentialFormatProfile.LdpVc, @@ -90,11 +95,11 @@ export class OpenId4VcIssuerService { public getIssuerMetadata(agentContext: AgentContext, issuerRecord: OpenId4VcIssuerRecord): IssuerMetadata { const config = agentContext.dependencyManager.resolve(OpenId4VcIssuerModuleConfig) - const issuerUrl = joinUriParts([config.baseUrl, issuerRecord.issuerId]) + const issuerUrl = joinUriParts(config.baseUrl, [issuerRecord.issuerId]) const issuerMetadata = { issuerUrl, - tokenEndpoint: joinUriParts([issuerUrl, config.accessTokenEndpoint.endpointPath]), - credentialEndpoint: joinUriParts([issuerUrl, config.credentialEndpoint.endpointPath]), + tokenEndpoint: joinUriParts(issuerUrl, [config.accessTokenEndpoint.endpointPath]), + credentialEndpoint: joinUriParts(issuerUrl, [config.credentialEndpoint.endpointPath]), credentialsSupported: issuerRecord.credentialsSupported, issuerDisplay: issuerRecord.display, } satisfies IssuerMetadata @@ -238,45 +243,34 @@ export class OpenId4VcIssuerService { private getJwtVerifyCallback = (agentContext: AgentContext): JWTVerifyCallback => { return async (opts) => { - const { jwt } = opts - - const { header, payload } = Jwt.fromSerializedJwt(jwt) - const { alg, kid, jwk: jwkJson } = header - - if (kid && jwkJson) { - throw new AriesFrameworkError('Either kid or jwk must be present, but not both') - } - - let jwk: Jwk - let didDocument: DidDocument | undefined = undefined - if (kid) { - if (!kid.startsWith('did:')) { - throw new AriesFrameworkError("Only kid with 'did:' prefix is supported for JWT") - } - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - didDocument = await didsApi.resolveDidDocument(kid) - const verificationMethod = didDocument.dereferenceKey(kid, ['authentication', 'assertionMethod']) - const key = getKeyFromVerificationMethod(verificationMethod) - jwk = getJwkFromKey(key) - } else if (jwkJson) { - jwk = getJwkFromJson(jwkJson) - } else { - throw new AriesFrameworkError('Either kid or jwk must be present') - } - - const { isValid } = await this.jwsService.verifyJws(agentContext, { - jws: jwt, - jwkResolver: () => jwk, + let didDocument = undefined as DidDocument | undefined + const { isValid, jws } = await this.jwsService.verifyJws(agentContext, { + jws: opts.jwt, + // Only handles kid as did resolution. JWK is handled by jws service + jwkResolver: async ({ protectedHeader: { kid } }) => { + if (!kid) throw new AriesFrameworkError('Missing kid in protected header.') + if (!kid.startsWith('did:')) throw new AriesFrameworkError('Only did is supported for kid identifier') + + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + didDocument = await didsApi.resolveDidDocument(kid) + const verificationMethod = didDocument.dereferenceKey(kid, ['authentication', 'assertionMethod']) + const key = getKeyFromVerificationMethod(verificationMethod) + return getJwkFromKey(key) + }, }) if (!isValid) throw new AriesFrameworkError('Could not verify JWT signature.') + // FIXME: the jws service should return some better decoded metadata also from the resolver + // as currently is less useful if you afterwards need properties from the JWS + const firstJws = jws.signatures[0] + const protectedHeader = JsonEncoder.fromBase64(firstJws.protected) return { - jwt: { header, payload: payload.toJson() }, - kid, - jwk: jwkJson, - did: kid, - alg, + jwt: { header: protectedHeader, payload: JsonEncoder.fromBase64(jws.payload) }, + kid: protectedHeader.kid, + jwk: protectedHeader.jwk ? getJwkFromJson(protectedHeader.jwk) : undefined, + did: didDocument?.id, + alg: protectedHeader.alg, didDocument, } } @@ -368,7 +362,7 @@ export class OpenId4VcIssuerService { private getSdJwtVcCredentialSigningCallback = ( agentContext: AgentContext, - options: SdJwtVcSignOptions + options: OpenId4VciSignSdJwtCredential ): CredentialSignerCallback => { return async () => { const sdJwtVcApi = getApiForModuleByName(agentContext, 'SdJwtVcModule') @@ -382,18 +376,22 @@ export class OpenId4VcIssuerService { private getW3cCredentialSigningCallback = ( agentContext: AgentContext, - options: W3cSignCredentialOptions + options: OpenId4VciSignW3cCredential ): CredentialSignerCallback => { return async (opts) => { - const { jwtVerifyResult } = opts - - // FIXME: how certain can we be that the key is verified to - // be in the did document? Where is the did resolved? - // I think in the jwtVerifyCallback we provide + const { jwtVerifyResult, format } = opts const { kid, didDocument: holderDidDocument } = jwtVerifyResult if (!kid) throw new AriesFrameworkError('Missing Kid. Cannot create the holder binding') if (!holderDidDocument) throw new AriesFrameworkError('Missing did document. Cannot create the holder binding.') + if (!format) throw new AriesFrameworkError('Missing format. Cannot issue credential.') + + const formatMap: Record = { + [OpenIdCredentialFormatProfile.JwtVcJson]: ClaimFormat.JwtVc, + [OpenIdCredentialFormatProfile.JwtVcJsonLd]: ClaimFormat.JwtVc, + [OpenIdCredentialFormatProfile.LdpVc]: ClaimFormat.LdpVc, + } + const w3cServiceFormat = formatMap[format] // Set the binding on the first credential subject if not set yet // on any subject @@ -404,20 +402,53 @@ export class OpenId4VcIssuerService { credentialSubject.id = holderDidDocument.id } - const signed = await this.w3cCredentialService.signCredential(agentContext, options) + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const issuerDidDocument = await didsApi.resolveDidDocument(options.verificationMethod) + const verificationMethod = issuerDidDocument.dereferenceVerificationMethod(options.verificationMethod) - return getSphereonW3cVerifiableCredential(signed) + if (w3cServiceFormat === ClaimFormat.JwtVc) { + const key = getKeyFromVerificationMethod(verificationMethod) + const alg = getJwkFromKey(key).supportedSignatureAlgorithms[0] + + if (!alg) { + throw new AriesFrameworkError(`No supported JWA signature algorithms for key type ${key.keyType}`) + } + + const signed = await this.w3cCredentialService.signCredential(agentContext, { + format: w3cServiceFormat, + credential: options.credential, + verificationMethod: options.verificationMethod, + alg, + }) + + return getSphereonW3cVerifiableCredential(signed) + } else { + const signed = await this.w3cCredentialService.signCredential(agentContext, { + format: w3cServiceFormat, + credential: options.credential, + verificationMethod: options.verificationMethod, + proofType: getProofTypeFromVerificationMethod(agentContext, verificationMethod), + }) + + return getSphereonW3cVerifiableCredential(signed) + } } } - private async getHolderBindingFromRequest(agentContext: AgentContext, credentialRequest: CredentialRequestV1_0_11) { + private async getHolderBindingFromRequest(credentialRequest: CredentialRequestV1_0_11) { if (!credentialRequest.proof?.jwt) throw new AriesFrameworkError('Received a credential request without a proof') const jwt = Jwt.fromSerializedJwt(credentialRequest.proof.jwt) if (jwt.header.kid) { - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - await didsApi.resolveDidDocument(jwt.header.kid) + if (!jwt.header.kid.startsWith('did:')) { + throw new AriesFrameworkError("Only did is supported for 'kid' identifier") + } else if (!jwt.header.kid.includes('#')) { + throw new AriesFrameworkError( + `kid containing did MUST point to a specific key within the did document: ${jwt.header.kid}` + ) + } + return { method: 'did', didUrl: jwt.header.kid, @@ -458,7 +489,7 @@ export class OpenId4VcIssuerService { let signOptions = options.credential if (!signOptions) { - const holderBinding = await this.getHolderBindingFromRequest(agentContext, credentialRequest) + const holderBinding = await this.getHolderBindingFromRequest(credentialRequest) signOptions = await this.openId4VcIssuerConfig.credentialEndpoint.credentialRequestToCredentialMapper({ agentContext, holderBinding, @@ -471,7 +502,7 @@ export class OpenId4VcIssuerService { } if (isW3cSignCredentialOptions(signOptions)) { - if (!W3cOpenId4VcFormats.includes(credentialRequest.format as OpenIdCredentialFormatProfile)) { + if (!w3cOpenId4VcFormats.includes(credentialRequest.format as OpenIdCredentialFormatProfile)) { throw new AriesFrameworkError( `The credential to be issued does not match the request. Cannot issue a W3cCredential if the client expects a credential of format '${credentialRequest.format}'.` ) @@ -497,7 +528,8 @@ export class OpenId4VcIssuerService { return { format: credentialRequest.format, // NOTE: we don't use the credential value here as we pass the credential directly to the singer - credential: null as unknown as CredentialIssuanceInput, + // FIXME: oid4vci adds `sub` property, but SD-JWT uses `cnf` + credential: { ...signOptions.payload } as unknown as CredentialIssuanceInput, signCallback: this.getSdJwtVcCredentialSigningCallback(agentContext, signOptions), } } @@ -505,6 +537,6 @@ export class OpenId4VcIssuerService { } } -function isW3cSignCredentialOptions(credential: OpenId4VciSignCredential): credential is W3cSignCredentialOptions { +function isW3cSignCredentialOptions(credential: OpenId4VciSignCredential): credential is OpenId4VciSignW3cCredential { return 'credential' in credential && credential.credential instanceof W3cCredential } diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts index 0bbe1d8a96..8883581fa5 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts @@ -1,6 +1,11 @@ import type { OpenId4VcIssuerRecordProps } from './repository/OpenId4VcIssuerRecord' -import type { OpenId4VciCredentialOffer, OpenId4VciCredentialRequest, OpenId4VciCredentialSupported } from '../shared' -import type { AgentContext, Jwk, W3cSignCredentialOptions } from '@aries-framework/core' +import type { + CredentialHolderBinding, + OpenId4VciCredentialOffer, + OpenId4VciCredentialRequest, + OpenId4VciCredentialSupported, +} from '../shared' +import type { AgentContext, W3cCredential } from '@aries-framework/core' import type { SdJwtVcSignOptions } from '@aries-framework/sd-jwt-vc' import type { CredentialOfferPayloadV1_0_11, CredentialSupported, MetadataDisplay } from '@sphereon/oid4vci-common' @@ -163,16 +168,9 @@ export type CredentialRequestToCredentialMapper = (options: { // w3c and sd-jwt services. However in that case you could also // ask why not just require the signed credential as output // as you can then just call the services yourself. -export type OpenId4VciSignCredential = SdJwtVcSignOptions | W3cSignCredentialOptions - -export type CredentialHolderDidBinding = { - method: 'did' - didUrl: string -} - -export type CredentialHolderJwkBinding = { - method: 'jwk' - jwk: Jwk +export type OpenId4VciSignCredential = OpenId4VciSignSdJwtCredential | OpenId4VciSignW3cCredential +export type OpenId4VciSignSdJwtCredential = SdJwtVcSignOptions +export interface OpenId4VciSignW3cCredential { + verificationMethod: string + credential: W3cCredential } - -export type CredentialHolderBinding = CredentialHolderDidBinding | CredentialHolderJwkBinding diff --git a/packages/openid4vc/src/openid4vc-issuer/__tests__/openId4vc-issuer-module.test.ts b/packages/openid4vc/src/openid4vc-issuer/__tests__/openId4vc-issuer-module.test.ts index 9e23a8daab..6d6929c847 100644 --- a/packages/openid4vc/src/openid4vc-issuer/__tests__/openId4vc-issuer-module.test.ts +++ b/packages/openid4vc/src/openid4vc-issuer/__tests__/openId4vc-issuer-module.test.ts @@ -1,11 +1,13 @@ -/* eslint-disable @typescript-eslint/unbound-method */ -import type { IssuerMetadata } from '../OpenId4VcIssuerServiceOptions' import type { DependencyManager } from '@aries-framework/core' +import { Router } from 'express' + +import { getAgentContext } from '../../../../core/tests' import { OpenId4VcIssuerApi } from '../OpenId4VcIssuerApi' import { OpenId4VcIssuerModule } from '../OpenId4VcIssuerModule' import { OpenId4VcIssuerModuleConfig } from '../OpenId4VcIssuerModuleConfig' import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' +import { OpenId4VcIssuerRepository } from '../repository/OpenId4VcIssuerRepository' const dependencyManager = { registerInstance: jest.fn(), @@ -14,29 +16,39 @@ const dependencyManager = { resolve: jest.fn().mockReturnValue({ logger: { warn: jest.fn() } }), } as unknown as DependencyManager +const agentContext = getAgentContext() + describe('OpenId4VcIssuerModule', () => { - test('registers dependencies on the dependency manager', () => { - const issuerMetadata: IssuerMetadata = { - issuerBaseUrl: 'https://example.com', - credentialEndpointPath: 'https://example.com/credentials', - tokenEndpointPath: 'https://example.com/token', - credentialsSupported: [], - } - const openId4VcClientModule = new OpenId4VcIssuerModule({ - issuerMetadata, - }) + test('registers dependencies on the dependency manager', async () => { + const options = { + baseUrl: 'http://localhost:3000', + endpoints: { + credential: { + credentialRequestToCredentialMapper: () => { + throw new Error('Not implemented') + }, + }, + }, + router: Router(), + } as const + const openId4VcClientModule = new OpenId4VcIssuerModule(options) openId4VcClientModule.register(dependencyManager) expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) expect(dependencyManager.registerInstance).toHaveBeenCalledWith( OpenId4VcIssuerModuleConfig, - new OpenId4VcIssuerModuleConfig({ issuerMetadata }) + new OpenId4VcIssuerModuleConfig(options) ) expect(dependencyManager.registerContextScoped).toHaveBeenCalledTimes(1) expect(dependencyManager.registerContextScoped).toHaveBeenCalledWith(OpenId4VcIssuerApi) - expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(2) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VcIssuerService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VcIssuerRepository) + + await openId4VcClientModule.initialize(agentContext) + + expect(openId4VcClientModule.config.router).toBeDefined() }) }) diff --git a/packages/openid4vc/src/openid4vc-issuer/router/metadataEndpoint.ts b/packages/openid4vc/src/openid4vc-issuer/router/metadataEndpoint.ts index 482b6ea245..a2a698f169 100644 --- a/packages/openid4vc/src/openid4vc-issuer/router/metadataEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-issuer/router/metadataEndpoint.ts @@ -6,7 +6,7 @@ import { getRequestContext, sendErrorResponse } from '../../shared/router' import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' export function configureIssuerMetadataEndpoint(router: Router) { - router.get('.well-known/openid-credential-issuer', (_request: IssuanceRequest, response: Response) => { + router.get('/.well-known/openid-credential-issuer', (_request: IssuanceRequest, response: Response) => { const { agentContext, issuer } = getRequestContext(_request) const openId4VcIssuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService) diff --git a/packages/openid4vc/src/openid4vc-issuer/router/requestContext.ts b/packages/openid4vc/src/openid4vc-issuer/router/requestContext.ts index e1b4cccdd3..36a4e466c8 100644 --- a/packages/openid4vc/src/openid4vc-issuer/router/requestContext.ts +++ b/packages/openid4vc/src/openid4vc-issuer/router/requestContext.ts @@ -18,7 +18,7 @@ export async function getAgentContextForIssuerId(rootAgentContext: AgentContext, // Check if multi-tenancy is enabled, and if so find the associated multi-tenant record // This is a bit hacky as it uses the tenants module to store the openid4vc issuer id // but this way we don't have to expose the contextCorrelationId in the issuer metadata - const tenantsApi = getApiForModuleByName(rootAgentContext, 'TenantsApi') + const tenantsApi = getApiForModuleByName(rootAgentContext, 'TenantsModule') if (tenantsApi) { const [tenant] = await tenantsApi.findTenantsByQuery({ [OPENID4VC_ISSUER_IDS_METADATA_KEY]: [issuerId], @@ -28,7 +28,7 @@ export async function getAgentContextForIssuerId(rootAgentContext: AgentContext, const agentContextProvider = rootAgentContext.dependencyManager.resolve( InjectionSymbols.AgentContextProvider ) - await agentContextProvider.getAgentContextForContextCorrelationId(tenant.id) + return agentContextProvider.getAgentContextForContextCorrelationId(tenant.id) } } @@ -49,13 +49,18 @@ export async function storeIssuerIdForContextCorrelationId(agentContext: AgentCo // It's kind of hacky, but we add support for the tenants module specifically here to map an issuerId to // a specific tenant. Otherwise we have to expose /:contextCorrelationId/:issuerId in all the public URLs // which is of course not so nice. - const tenantsApi = getApiForModuleByName(agentContext, 'TenantsApi') + // FIXME: it's maybe nicer to just depend on the tenants module + const tenantsApi = getApiForModuleByName(agentContext, 'TenantsModule') + // We don't want to query the tenant record if the current context is the root context if (tenantsApi && tenantsApi.rootAgentContext.contextCorrelationId !== agentContext.contextCorrelationId) { const tenantRecord = await tenantsApi.getTenantById(agentContext.contextCorrelationId) - const openId4VcIssuerIds = tenantRecord.metadata.get(OPENID4VC_ISSUER_IDS_METADATA_KEY) ?? [] - tenantRecord.metadata.set(OPENID4VC_ISSUER_IDS_METADATA_KEY, [...openId4VcIssuerIds, issuerId]) + const currentOpenId4VcIssuerIds = tenantRecord.metadata.get(OPENID4VC_ISSUER_IDS_METADATA_KEY) ?? [] + const openId4VcIssuerIds = [...currentOpenId4VcIssuerIds, issuerId] + + tenantRecord.metadata.set(OPENID4VC_ISSUER_IDS_METADATA_KEY, openId4VcIssuerIds) + tenantRecord.setTag(OPENID4VC_ISSUER_IDS_METADATA_KEY, openId4VcIssuerIds) await tenantsApi.updateTenant(tenantRecord) } } diff --git a/packages/openid4vc/src/shared/models/CredentialHolderBinding.ts b/packages/openid4vc/src/shared/models/CredentialHolderBinding.ts new file mode 100644 index 0000000000..0796e9b213 --- /dev/null +++ b/packages/openid4vc/src/shared/models/CredentialHolderBinding.ts @@ -0,0 +1,13 @@ +import type { Jwk } from '@aries-framework/core' + +export type CredentialHolderDidBinding = { + method: 'did' + didUrl: string +} + +export type CredentialHolderJwkBinding = { + method: 'jwk' + jwk: Jwk +} + +export type CredentialHolderBinding = CredentialHolderDidBinding | CredentialHolderJwkBinding diff --git a/packages/openid4vc/src/shared/models/index.ts b/packages/openid4vc/src/shared/models/index.ts index b3223a1aea..2c8abfdd02 100644 --- a/packages/openid4vc/src/shared/models/index.ts +++ b/packages/openid4vc/src/shared/models/index.ts @@ -1,5 +1,8 @@ import type { AssertedUniformCredentialOffer, + CredentialRequestJwtVcJson, + CredentialRequestJwtVcJsonLdAndLdpVc, + CredentialRequestSdJwtVc, CredentialSupported, UniformCredentialRequest, } from '@sphereon/oid4vci-common' @@ -7,4 +10,9 @@ import type { export type OpenId4VciCredentialSupportedWithId = CredentialSupported & { id: string } export type OpenId4VciCredentialSupported = CredentialSupported export type OpenId4VciCredentialRequest = UniformCredentialRequest +export type OpenId4VciCredentialRequestJwtVcJson = CredentialRequestJwtVcJson +export type OpenId4VciCredentialRequestJwtVcJsonLdAndLdpVc = CredentialRequestJwtVcJsonLdAndLdpVc +export type OpenId4VciCredentialRequestSdJwtVc = CredentialRequestSdJwtVc export type OpenId4VciCredentialOffer = AssertedUniformCredentialOffer + +export * from './CredentialHolderBinding' diff --git a/packages/openid4vc/src/shared/router.ts b/packages/openid4vc/src/shared/router.ts index e1354e63fb..5073f7a348 100644 --- a/packages/openid4vc/src/shared/router.ts +++ b/packages/openid4vc/src/shared/router.ts @@ -12,7 +12,9 @@ export function sendErrorResponse(response: Response, logger: Logger, code: numb error instanceof Error ? error.message : typeof error === 'string' ? error : 'An unknown error occurred.' const body = { error: message, error_description } - logger.warn(`[OID4VCI] Sending error response: ${JSON.stringify(body)}`) + logger.warn(`[OID4VCI] Sending error response: ${JSON.stringify(body)}`, { + error, + }) return response.status(code).json(body) } diff --git a/packages/openid4vc/tests/openid4vc.e2e.test.ts b/packages/openid4vc/tests/openid4vc.e2e.test.ts index 6e16cc135b..656cffade3 100644 --- a/packages/openid4vc/tests/openid4vc.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc.e2e.test.ts @@ -1,6 +1,6 @@ -import type { IssuerMetadata } from './../src/openid4vc-issuer' import type { AgentType, TenantType } from './utils' -import type { CreateProofRequestOptions } from '../src' +import type { CreateProofRequestOptions, CredentialBindingResolver } from '../src' +import type { SdJwtVc, SdJwtVcSignOptions } from '@aries-framework/sd-jwt-vc' import type { Server } from 'http' import { AskarModule } from '@aries-framework/askar' @@ -12,19 +12,21 @@ import { W3cCredentialSubject, W3cIssuer, w3cDate, + DidsApi, + getKeyFromVerificationMethod, + getJwkFromKey, + AriesFrameworkError, } from '@aries-framework/core' import { SdJwtVcModule } from '@aries-framework/sd-jwt-vc' import { TenantsModule } from '@aries-framework/tenants' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import express, { Router, type Express } from 'express' -import { SdJwtCredential } from '../../sd-jwt-vc/src/SdJwtCredential' -import { OpenId4VcVerifierModule } from '../src' -import { OpenId4VcHolderModule } from '../src/openid4vc-holder' -import { OpenId4VcIssuerModule } from '../src/openid4vc-issuer' +import { askarModuleConfig } from '../../askar/tests/helpers' +import { OpenId4VcVerifierModule, OpenId4VcHolderModule, OpenId4VcIssuerModule } from '../src' import { createAgentFromModules, createTenantForAgent } from './utils' -import { allCredentialsSupported, universityDegreeCredentialSdJwt, universityDegreeCredentialSdJwt2 } from './utilsVci' +import { universityDegreeCredentialSdJwt, universityDegreeCredentialSdJwt2 } from './utilsVci' import { openBadgePresentationDefinition, staticOpOpenIdConfigEdDSA, @@ -33,29 +35,56 @@ import { } from './utilsVp' const issuerPort = 1234 -const baseUrl = `http://localhost:${issuerPort}` +const baseUrl = `http://localhost:${issuerPort}/oid4vci` -const baseCredentialRequestOptions = { +const baseCredentialOfferOptions = { scheme: 'openid-credential-offer', baseUri: baseUrl, } -const issuerMetadata: IssuerMetadata = { - issuerBaseUrl: baseUrl, - credentialEndpointPath: `/credentials`, - tokenEndpointPath: `/token`, - credentialsSupported: allCredentialsSupported, -} const holderModules = { openId4VcHolder: new OpenId4VcHolderModule(), sdJwtVc: new SdJwtVcModule(), - askar: new AskarModule({ ariesAskar }), + askar: new AskarModule(askarModuleConfig), } as const +const oid4vciRouter = Router() const issuerModules = { - openId4VcIssuer: new OpenId4VcIssuerModule({ issuerMetadata }), + openId4VcIssuer: new OpenId4VcIssuerModule({ + baseUrl, + router: oid4vciRouter, + endpoints: { + credential: { + // FIXME: should not be nested under the endpoint config, as it's also used for the non-endpoint part + credentialRequestToCredentialMapper: async ({ agentContext, credentialRequest, holderBinding }) => { + // We sign the request with the first did:key did we have + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const [firstDidKeyDid] = await didsApi.getCreatedDids({ method: 'key' }) + const didDocument = await didsApi.resolveDidDocument(firstDidKeyDid.did) + const verificationMethod = didDocument.verificationMethod?.[0] + if (!verificationMethod) { + throw new Error('No verification method found') + } + + if (credentialRequest.format === 'vc+sd-jwt') { + return { + payload: { vct: credentialRequest.vct, university: 'innsbruck', degree: 'bachelor' }, + holder: holderBinding, + issuer: { + method: 'did', + didUrl: verificationMethod.id, + }, + disclosureFrame: { university: true, degree: true }, + } satisfies SdJwtVcSignOptions + } + + throw new Error('Invalid request') + }, + }, + }, + }), sdJwtVc: new SdJwtVcModule(), - askar: new AskarModule({ ariesAskar }), + askar: new AskarModule(askarModuleConfig), } as const const verifierModules = { @@ -71,45 +100,47 @@ const verifierModules = { describe('OpenId4Vc', () => { let expressApp: Express - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let expressServer: Server + let expressServer: Server let issuer: AgentType }> - let issuer1: TenantType - let issuer2: TenantType + let issuer1: TenantType + let issuer2: TenantType let holder: AgentType }> - let holder1: TenantType + let holder1: TenantType let verifier: AgentType }> - let verifier1: TenantType - let verifier2: TenantType + let verifier1: TenantType + let verifier2: TenantType beforeEach(async () => { expressApp = express() + expressApp.use('/oid4vci', oid4vciRouter) issuer = await createAgentFromModules( 'issuer', - { ...issuerModules, tenants: new TenantsModule() }, + { ...issuerModules, tenants: new TenantsModule() }, '96213c3d7fc8d4d6754c7a0fd969598g' ) - issuer1 = await createTenantForAgent(issuer.agent as any, 'iTenant1') - issuer2 = await createTenantForAgent(issuer.agent as any, 'iTenant2') + issuer1 = await createTenantForAgent(issuer.agent, 'iTenant1') + issuer2 = await createTenantForAgent(issuer.agent, 'iTenant2') holder = await createAgentFromModules( 'holder', { ...holderModules, tenants: new TenantsModule() }, '96213c3d7fc8d4d6754c7a0fd969598e' ) - holder1 = await createTenantForAgent(holder.agent as any, 'hTenant1') + holder1 = await createTenantForAgent(holder.agent, 'hTenant1') verifier = await createAgentFromModules( 'verifier', { ...verifierModules, tenants: new TenantsModule() }, '96213c3d7fc8d4d6754c7a0fd969598f' ) - verifier1 = await createTenantForAgent(verifier.agent as any, 'vTenant1') - verifier2 = await createTenantForAgent(verifier.agent as any, 'vTenant2') + verifier1 = await createTenantForAgent(verifier.agent, 'vTenant1') + verifier2 = await createTenantForAgent(verifier.agent, 'vTenant2') + + expressServer = expressApp.listen(issuerPort) }) afterEach(async () => { @@ -122,92 +153,54 @@ describe('OpenId4Vc', () => { await holder.agent.wallet.delete() }) - it('e2e flow with tenants, issuer endpoints requesting a sdjwtvc', async () => { - const issuerTenant1 = await issuer.agent.modules.tenants.getTenantAgent({ tenantId: issuer1.tenantId }) - const issuer1Router = Router() - const issuer1BasePath = '/issuer1' + const credentialBindingResolver: CredentialBindingResolver = ({ supportsJwk, supportedDidMethods }) => { + // prefer did:key + if (supportedDidMethods?.includes('did:key')) { + return { + method: 'did', + didUrl: holder1.verificationMethod.id, + } + } - await issuerTenant1.modules.openId4VcIssuer.configureRouter(issuer1Router, { - basePath: issuer1BasePath, - metadataEndpointConfig: { enabled: true }, - accessTokenEndpointConfig: { - enabled: true, - preAuthorizedCodeExpirationDuration: 50, - verificationMethod: issuer1.verificationMethod, - }, - credentialEndpointConfig: { - enabled: true, - verificationMethod: issuer1.verificationMethod, - credentialRequestToCredentialMapper: async ({ credentialRequest, holderDid, holderDidUrl }) => { - if ( - credentialRequest.format === 'vc+sd-jwt' && - credentialRequest.credential_definition.vct === 'UniversityDegreeCredential' - ) { - if (holderDid !== holder1.did) throw new Error('Invalid holder did') - - return new SdJwtCredential({ - payload: { type: 'UniversityDegreeCredential', university: 'innsbruck', degree: 'bachelor' }, - holderDidUrl: holderDidUrl, - issuerDidUrl: issuer1.kid, - disclosureFrame: { university: true, degree: true }, - }) - } + // otherwise fall back to JWK + if (supportsJwk) { + return { + method: 'jwk', + jwk: getJwkFromKey(getKeyFromVerificationMethod(holder1.verificationMethod)), + } + } - throw new Error('Invalid request') - }, - }, - }) + console.log(supportsJwk, supportedDidMethods) - const issuerTenant2 = await issuer.agent.modules.tenants.getTenantAgent({ tenantId: issuer2.tenantId }) - const issuer2Router = Router() - const issuer2BasePath = '/issuer2' + // otherwise throw an error + throw new AriesFrameworkError('Issuer does not support did:key or JWK for credential binding') + } - await issuerTenant2.modules.openId4VcIssuer.configureRouter(issuer2Router, { - basePath: issuer2BasePath, - metadataEndpointConfig: { enabled: true }, - accessTokenEndpointConfig: { - enabled: true, - preAuthorizedCodeExpirationDuration: 50, - verificationMethod: issuer2.verificationMethod, - }, - credentialEndpointConfig: { - enabled: true, - verificationMethod: issuer2.verificationMethod, - credentialRequestToCredentialMapper: async ({ credentialRequest, holderDid, holderDidUrl }) => { - if ( - credentialRequest.format === 'vc+sd-jwt' && - credentialRequest.credential_definition.vct === 'UniversityDegreeCredential2' - ) { - if (holderDid !== holder1.did) throw new Error('Invalid holder did') - - return new SdJwtCredential({ - payload: { type: 'UniversityDegreeCredential2', university: 'innsbruck', degree: 'bachelor' }, - holderDidUrl: holderDidUrl, - issuerDidUrl: issuer2.kid, - disclosureFrame: { university: true, degree: true }, - }) - } + it('e2e flow with tenants, issuer endpoints requesting a sd-jwt-vc', async () => { + const issuerTenant1 = await issuer.agent.modules.tenants.getTenantAgent({ tenantId: issuer1.tenantId }) + const issuerTenant2 = await issuer.agent.modules.tenants.getTenantAgent({ tenantId: issuer2.tenantId }) - throw new Error('Invalid request') - }, - }, + const openIdIssuerTenant1 = await issuerTenant1.modules.openId4VcIssuer.createIssuer({ + credentialsSupported: [universityDegreeCredentialSdJwt], }) - expressApp.use(issuer1BasePath, issuer1Router) - expressApp.use(issuer2BasePath, issuer2Router) - expressServer = expressApp.listen(issuerPort) + const openIdIssuerTenant2 = await issuerTenant2.modules.openId4VcIssuer.createIssuer({ + credentialsSupported: [universityDegreeCredentialSdJwt2], + }) - const { credentialOfferRequest: credentialOfferRequest1 } = - await issuerTenant1.modules.openId4VcIssuer.createCredentialOfferAndRequest( - [universityDegreeCredentialSdJwt.id], - { preAuthorizedCodeFlowConfig: { userPinRequired: false }, ...baseCredentialRequestOptions } - ) + const { credentialOfferUri: credentialOffer1 } = await issuerTenant1.modules.openId4VcIssuer.createCredentialOffer({ + issuerId: openIdIssuerTenant1.issuerId, + offeredCredentials: [universityDegreeCredentialSdJwt.id], + preAuthorizedCodeFlowConfig: { userPinRequired: false }, + ...baseCredentialOfferOptions, + }) - const { credentialOfferRequest: credentialOfferRequest2 } = - await issuerTenant2.modules.openId4VcIssuer.createCredentialOfferAndRequest( - [universityDegreeCredentialSdJwt2.id], - { preAuthorizedCodeFlowConfig: { userPinRequired: false }, ...baseCredentialRequestOptions } - ) + const { credentialOfferUri: credentialOffer2 } = await issuerTenant2.modules.openId4VcIssuer.createCredentialOffer({ + issuerId: openIdIssuerTenant2.issuerId, + offeredCredentials: [universityDegreeCredentialSdJwt2.id], + preAuthorizedCodeFlowConfig: { userPinRequired: false }, + ...baseCredentialOfferOptions, + }) await issuerTenant1.endSession() await issuerTenant2.endSession() @@ -215,58 +208,62 @@ describe('OpenId4Vc', () => { const holderTenant1 = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId }) const resolvedCredentialOffer1 = await holderTenant1.modules.openId4VcHolder.resolveCredentialOffer( - credentialOfferRequest1 + credentialOffer1 ) - expect(resolvedCredentialOffer1.credentialOfferPayload.credential_issuer).toEqual(`${baseUrl}/issuer1`) + expect(resolvedCredentialOffer1.credentialOfferPayload.credential_issuer).toEqual( + `${baseUrl}/${openIdIssuerTenant1.issuerId}` + ) expect(resolvedCredentialOffer1.metadata.credentialIssuerMetadata?.token_endpoint).toEqual( - `${baseUrl}/issuer1/token` + `${baseUrl}/${openIdIssuerTenant1.issuerId}/token` ) expect(resolvedCredentialOffer1.metadata.credentialIssuerMetadata?.credential_endpoint).toEqual( - `${baseUrl}/issuer1/credentials` + `${baseUrl}/${openIdIssuerTenant1.issuerId}/credential` ) - const credentials1 = await holderTenant1.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + // Bind to JWK + const credentialsTenant1 = await holderTenant1.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( resolvedCredentialOffer1, { - proofOfPossessionVerificationMethodResolver: async () => { - return holder1.verificationMethod - }, + credentialBindingResolver, } ) + expect(credentialsTenant1).toHaveLength(1) + const compactSdJwtVcTenant1 = (credentialsTenant1[0] as SdJwtVc).compact + const sdJwtVcTenant1 = await holderTenant1.modules.sdJwtVc.fromCompact(compactSdJwtVcTenant1) + expect(sdJwtVcTenant1.payload.vct).toEqual('UniversityDegreeCredential') + const resolvedCredentialOffer2 = await holderTenant1.modules.openId4VcHolder.resolveCredentialOffer( - credentialOfferRequest2 + credentialOffer2 + ) + expect(resolvedCredentialOffer2.credentialOfferPayload.credential_issuer).toEqual( + `${baseUrl}/${openIdIssuerTenant2.issuerId}` ) - expect(resolvedCredentialOffer2.credentialOfferPayload.credential_issuer).toEqual(`${baseUrl}/issuer2`) expect(resolvedCredentialOffer2.metadata.credentialIssuerMetadata?.token_endpoint).toEqual( - `${baseUrl}/issuer2/token` + `${baseUrl}/${openIdIssuerTenant2.issuerId}/token` ) expect(resolvedCredentialOffer2.metadata.credentialIssuerMetadata?.credential_endpoint).toEqual( - `${baseUrl}/issuer2/credentials` + `${baseUrl}/${openIdIssuerTenant2.issuerId}/credential` ) - expect(credentials1).toHaveLength(1) - if (credentials1[0].type === 'W3cCredentialRecord') throw new Error('Invalid credential type') - expect(credentials1[0].sdJwtVc.payload['type']).toEqual('UniversityDegreeCredential') - - const credentials2 = await holderTenant1.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( + // Bind to did + const credentialsTenant2 = await holderTenant1.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( resolvedCredentialOffer2, { - proofOfPossessionVerificationMethodResolver: async () => { - return holder1.verificationMethod - }, + credentialBindingResolver, } ) - expect(credentials2).toHaveLength(1) - if (credentials2[0].type === 'W3cCredentialRecord') throw new Error('Invalid credential type') - expect(credentials2[0].sdJwtVc.payload['type']).toEqual('UniversityDegreeCredential2') + expect(credentialsTenant2).toHaveLength(1) + const compactSdJwtVcTenant2 = (credentialsTenant2[0] as SdJwtVc).compact + const sdJwtVcTenant2 = await holderTenant1.modules.sdJwtVc.fromCompact(compactSdJwtVcTenant2) + expect(sdJwtVcTenant2.payload.vct).toEqual('UniversityDegreeCredential2') await holderTenant1.endSession() }) - it('e2e flow with tenants, verifier endpoints verifying a sdjwtvc', async () => { + xit('e2e flow with tenants, verifier endpoints verifying a sd-jwt-vc', async () => { const mockFunction1 = jest.fn() mockFunction1.mockReturnValue({ status: 200 }) diff --git a/packages/openid4vc/tests/utils.ts b/packages/openid4vc/tests/utils.ts index 71bb4f0093..725f909ca7 100644 --- a/packages/openid4vc/tests/utils.ts +++ b/packages/openid4vc/tests/utils.ts @@ -1,13 +1,13 @@ -import type { BaseAgent, EmptyModuleMap, KeyDidCreateOptions, ModulesMap } from '@aries-framework/core' +import type { TenantAgent } from '../../tenants/src/TenantAgent' +import type { KeyDidCreateOptions, ModulesMap } from '@aries-framework/core' import type { TenantsModule } from '@aries-framework/tenants' -import { Agent, DidKey, KeyType, TypedArrayEncoder, utils } from '@aries-framework/core' +import { LogLevel, Agent, DidKey, KeyType, TypedArrayEncoder, utils } from '@aries-framework/core' import { agentDependencies } from '@aries-framework/node' -export async function createDidKidVerificationMethod>( - agent: A, - secretKey: string -) { +import { TestLogger } from '../../core/tests/logger' + +export async function createDidKidVerificationMethod(agent: Agent | TenantAgent, secretKey: string) { const didCreateResult = await agent.dids.create({ method: 'key', options: { keyType: KeyType.Ed25519 }, @@ -30,13 +30,13 @@ export async function createDidKidVerificationMethod(label: string, modulesMap: MM, secretKey: string) { const agent = new Agent({ - config: { label, walletConfig: { id: utils.uuid(), key: utils.uuid() } }, + config: { label, walletConfig: { id: utils.uuid(), key: utils.uuid() }, logger: new TestLogger(LogLevel.off) }, dependencies: agentDependencies, modules: modulesMap, }) await agent.initialize() - const data = await createDidKidVerificationMethod(agent, secretKey) + const data = await createDidKidVerificationMethod(agent, secretKey) return { ...data, @@ -46,8 +46,14 @@ export async function createAgentFromModules(label: strin export type AgentType = Awaited>> -export async function createTenantForAgent( - agent: Agent<{ tenants: TenantsModule }>, +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type AgentWithTenantsModule = Agent<{ tenants: TenantsModule }> + +export async function createTenantForAgent( + // FIXME: we need to make some improvements on the agent typing. It'a quite hard + // to get it right at the moment + // eslint-disable-next-line @typescript-eslint/no-explicit-any + agent: AgentWithTenantsModule & any, label: string ) { const tenantRecord = await agent.modules.tenants.createTenant({ @@ -61,7 +67,7 @@ export async function createTenantForAgent( const secretKey = (nonce1 + nonce2).slice(0, 32) const tenant = await agent.modules.tenants.getTenantAgent({ tenantId: tenantRecord.id }) - const data = await createDidKidVerificationMethod(tenant, secretKey) + const data = await createDidKidVerificationMethod(tenant, secretKey) await tenant.endSession() return { @@ -70,4 +76,4 @@ export async function createTenantForAgent( } } -export type TenantType = Awaited>> +export type TenantType = Awaited> diff --git a/packages/openid4vc/tests/utilsVci.ts b/packages/openid4vc/tests/utilsVci.ts index 3652b4d42c..f3f764a03f 100644 --- a/packages/openid4vc/tests/utilsVci.ts +++ b/packages/openid4vc/tests/utilsVci.ts @@ -25,12 +25,15 @@ export const universityDegreeCredentialSdJwt = { id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredentialSdJwt', format: OpenIdCredentialFormatProfile.SdJwtVc, vct: 'UniversityDegreeCredential', + cryptographic_binding_methods_supported: ['did:key'], } satisfies OpenId4VciCredentialSupportedWithId export const universityDegreeCredentialSdJwt2 = { id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredentialSdJwt2', format: OpenIdCredentialFormatProfile.SdJwtVc, vct: 'UniversityDegreeCredential2', + // FIXME: should this be dynamically generated? I think static is fine for now + cryptographic_binding_methods_supported: ['jwk'], } satisfies OpenId4VciCredentialSupportedWithId export const allCredentialsSupported = [ diff --git a/packages/sd-jwt-vc/src/SdJwtVcService.ts b/packages/sd-jwt-vc/src/SdJwtVcService.ts index fcba3f079b..9cf365fe1d 100644 --- a/packages/sd-jwt-vc/src/SdJwtVcService.ts +++ b/packages/sd-jwt-vc/src/SdJwtVcService.ts @@ -158,7 +158,7 @@ export class SdJwtVcService { agentContext: AgentContext, { compactSdJwtVc, keyBinding, requiredClaimKeys }: SdJwtVcVerifyOptions ) { - const sdJwtVc = _SdJwtVc.fromCompact(compactSdJwtVc) + const sdJwtVc = _SdJwtVc.fromCompact(compactSdJwtVc).withHasher(this.hasher) const issuer = await this.extractKeyFromIssuer(agentContext, this.parseIssuerFromCredential(sdJwtVc)) const holder = await this.extractKeyFromHolderBinding(agentContext, this.parseHolderBindingFromCredential(sdJwtVc)) @@ -198,6 +198,12 @@ export class SdJwtVcService { return { verification: verificationResult, + sdJwtVc: { + payload: sdJwtVc.payload, + header: sdJwtVc.header, + compact: compactSdJwtVc, + prettyClaims: await sdJwtVc.getPrettyClaims(), + } satisfies SdJwtVc, } } From e210e09ec9830adeb8b960a22997ee622a07b2e5 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Fri, 12 Jan 2024 16:30:45 +0700 Subject: [PATCH 096/115] some additional updates Signed-off-by: Timo Glastra --- demo-openid/src/Issuer.ts | 12 ++++---- .../reception/OpenId4VciHolderService.ts | 29 ++++++++++--------- .../OpenId4VciHolderServiceOptions.ts | 14 ++++----- .../reception/utils/Formats.ts | 14 ++++----- .../reception/utils/IssuerMetadataUtils.ts | 24 +++++++-------- .../__tests__/claimFormatMapping.test.ts | 20 ++++++------- .../reception/utils/claimFormatMapping.ts | 16 +++++----- .../OpenId4VcIssuerService.ts | 28 +++++++++--------- .../OpenId4VcVerifierService.ts | 17 +++++------ packages/openid4vc/tests/utilsVci.ts | 12 ++++---- 10 files changed, 93 insertions(+), 93 deletions(-) diff --git a/demo-openid/src/Issuer.ts b/demo-openid/src/Issuer.ts index db70b69cf5..203e49a322 100644 --- a/demo-openid/src/Issuer.ts +++ b/demo-openid/src/Issuer.ts @@ -16,7 +16,7 @@ import { W3cIssuer, w3cDate, } from '@aries-framework/core' -import { OpenId4VcIssuerModule, OpenIdCredentialFormatProfile } from '@aries-framework/openid4vc' +import { OpenId4VcIssuerModule, OpenId4VciCredentialFormatProfile } from '@aries-framework/openid4vc' import { SdJwtVcModule } from '@aries-framework/sd-jwt-vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { Router } from 'express' @@ -26,19 +26,19 @@ import { Output } from './OutputClass' export const universityDegreeCredential = { id: 'UniversityDegreeCredential', - format: OpenIdCredentialFormatProfile.JwtVcJson, + format: OpenId4VciCredentialFormatProfile.JwtVcJson, types: ['VerifiableCredential', 'UniversityDegreeCredential'], } satisfies OpenId4VciCredentialSupportedWithId export const openBadgeCredential = { id: 'OpenBadgeCredential', - format: OpenIdCredentialFormatProfile.JwtVcJson, + format: OpenId4VciCredentialFormatProfile.JwtVcJson, types: ['VerifiableCredential', 'OpenBadgeCredential'], } satisfies OpenId4VciCredentialSupportedWithId export const universityDegreeCredentialSdJwt = { id: 'UniversityDegreeCredential-sdjwt', - format: OpenIdCredentialFormatProfile.SdJwtVc, + format: OpenId4VciCredentialFormatProfile.SdJwtVc, vct: 'UniversityDegreeCredential', } satisfies OpenId4VciCredentialSupportedWithId @@ -65,8 +65,6 @@ function getCredentialRequestToCredentialMapper({ issuer: new W3cIssuer({ id: issuerDidKey.did, }), - // NOTE: credentialSubject will be set at lower level as well, but we can also set it here - // FIXME: we should also set cnf like we set credentialSubject.id credentialSubject: new W3cCredentialSubject({ id: parseDid(holderBinding.didUrl).did, }), @@ -154,7 +152,7 @@ export class Issuer extends BaseAgent<{ public async createCredentialOffer(offeredCredentials: string[]) { const { credentialOfferUri } = await this.agent.modules.openId4VcIssuer.createCredentialOffer({ - issuerId: this.issuerRecord.id, + issuerId: this.issuerRecord.issuerId, offeredCredentials, scheme: 'openid-credential-offer', preAuthorizedCodeFlowConfig: { userPinRequired: false }, diff --git a/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderService.ts b/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderService.ts index 53b90eba48..e4b6080c3a 100644 --- a/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderService.ts @@ -66,7 +66,7 @@ import { type SupportedCredentialFormats, supportedCredentialFormats, } from './OpenId4VciHolderServiceOptions' -import { OpenIdCredentialFormatProfile } from './utils' +import { OpenId4VciCredentialFormatProfile } from './utils' import { getFormatForVersion, getUniformFormat } from './utils/Formats' import { getMetadataFromCredentialOffer, @@ -243,9 +243,12 @@ export class OpenId4VciHolderService { } const locations = authDetailsLocation ? [authDetailsLocation] : undefined - if (format === OpenIdCredentialFormatProfile.JwtVcJson) { + if (format === OpenId4VciCredentialFormatProfile.JwtVcJson) { return { type, format, types, locations } - } else if (format === OpenIdCredentialFormatProfile.LdpVc || format === OpenIdCredentialFormatProfile.JwtVcJsonLd) { + } else if ( + format === OpenId4VciCredentialFormatProfile.LdpVc || + format === OpenId4VciCredentialFormatProfile.JwtVcJsonLd + ) { // Inline Credential Offers come with no context so we cannot create the authorization_details // This type of credentials can only be requested via scopes if (offerType === OfferedCredentialType.InlineCredentialOffer) return undefined @@ -257,7 +260,7 @@ export class OpenId4VciHolderService { } return { type, format, locations, credential_definition } - } else if (format === OpenIdCredentialFormatProfile.SdJwtVc) { + } else if (format === OpenId4VciCredentialFormatProfile.SdJwtVc) { return { type, format, @@ -618,14 +621,14 @@ export class OpenId4VciHolderService { signatureAlgorithm = options.possibleProofOfPossessionSignatureAlgorithms[0] } else { switch (credentialsToRequest.format) { - case OpenIdCredentialFormatProfile.JwtVcJson: - case OpenIdCredentialFormatProfile.JwtVcJsonLd: - case OpenIdCredentialFormatProfile.SdJwtVc: + case OpenId4VciCredentialFormatProfile.JwtVcJson: + case OpenId4VciCredentialFormatProfile.JwtVcJsonLd: + case OpenId4VciCredentialFormatProfile.SdJwtVc: signatureAlgorithm = options.possibleProofOfPossessionSignatureAlgorithms.find((signatureAlgorithm) => issuerSupportedCryptographicSuites.includes(signatureAlgorithm) ) break - case OpenIdCredentialFormatProfile.LdpVc: + case OpenId4VciCredentialFormatProfile.LdpVc: signatureAlgorithm = options.possibleProofOfPossessionSignatureAlgorithms.find((signatureAlgorithm) => { const JwkClass = getJwkClassFromJwaSignatureAlgorithm(signatureAlgorithm) if (!JwkClass) return false @@ -674,11 +677,11 @@ export class OpenId4VciHolderService { } const format = getUniformFormat(credentialResponse.successBody.format) - if (format === OpenIdCredentialFormatProfile.SdJwtVc) { + if (format === OpenId4VciCredentialFormatProfile.SdJwtVc) { if (typeof credentialResponse.successBody.credential !== 'string') throw new AriesFrameworkError( `Received a credential of format ${ - OpenIdCredentialFormatProfile.SdJwtVc + OpenId4VciCredentialFormatProfile.SdJwtVc }, but the credential is not a string. ${JSON.stringify(credentialResponse.successBody.credential)}` ) @@ -697,8 +700,8 @@ export class OpenId4VciHolderService { return sdJwtVc } else if ( - format === OpenIdCredentialFormatProfile.JwtVcJson || - format === OpenIdCredentialFormatProfile.JwtVcJsonLd + format === OpenId4VciCredentialFormatProfile.JwtVcJson || + format === OpenId4VciCredentialFormatProfile.JwtVcJsonLd ) { const credential = W3cJwtVerifiableCredential.fromSerializedJwt( credentialResponse.successBody.credential as string @@ -713,7 +716,7 @@ export class OpenId4VciHolderService { } return credential - } else if (format === OpenIdCredentialFormatProfile.LdpVc) { + } else if (format === OpenId4VciCredentialFormatProfile.LdpVc) { const credential = W3cJsonLdVerifiableCredential.fromJson( credentialResponse.successBody.credential as Record ) diff --git a/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderServiceOptions.ts b/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderServiceOptions.ts index df88d6593d..c04bff021e 100644 --- a/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderServiceOptions.ts @@ -8,17 +8,17 @@ import type { AuthorizationDetails, } from '@sphereon/oid4vci-common' -import { OpenIdCredentialFormatProfile } from './utils/claimFormatMapping' +import { OpenId4VciCredentialFormatProfile } from './utils/claimFormatMapping' export type SupportedCredentialFormats = - | OpenIdCredentialFormatProfile.JwtVcJson - | OpenIdCredentialFormatProfile.JwtVcJsonLd - | OpenIdCredentialFormatProfile.SdJwtVc + | OpenId4VciCredentialFormatProfile.JwtVcJson + | OpenId4VciCredentialFormatProfile.JwtVcJsonLd + | OpenId4VciCredentialFormatProfile.SdJwtVc export const supportedCredentialFormats: SupportedCredentialFormats[] = [ - OpenIdCredentialFormatProfile.JwtVcJson, - OpenIdCredentialFormatProfile.JwtVcJsonLd, - OpenIdCredentialFormatProfile.SdJwtVc, + OpenId4VciCredentialFormatProfile.JwtVcJson, + OpenId4VciCredentialFormatProfile.JwtVcJsonLd, + OpenId4VciCredentialFormatProfile.SdJwtVc, ] export type { OpenId4VCIVersion, EndpointMetadataResult, CredentialOfferPayloadV1_0_11, AuthorizationDetails } diff --git a/packages/openid4vc/src/openid4vc-holder/reception/utils/Formats.ts b/packages/openid4vc/src/openid4vc-holder/reception/utils/Formats.ts index 67151fee37..660b7df4fa 100644 --- a/packages/openid4vc/src/openid4vc-holder/reception/utils/Formats.ts +++ b/packages/openid4vc/src/openid4vc-holder/reception/utils/Formats.ts @@ -3,28 +3,28 @@ import type { CredentialFormat } from '@sphereon/ssi-types' import { AriesFrameworkError } from '@aries-framework/core' import { OpenId4VCIVersion } from '@sphereon/oid4vci-common' -import { OpenIdCredentialFormatProfile } from './claimFormatMapping' +import { OpenId4VciCredentialFormatProfile } from './claimFormatMapping' // Based on https://github.com/Sphereon-Opensource/OID4VCI/pull/54/files // check if a string is a valid enum value of OpenIdCredentialFormatProfile -const isUniformFormat = (format: string): format is OpenIdCredentialFormatProfile => { - return Object.values(OpenIdCredentialFormatProfile).includes(format as OpenIdCredentialFormatProfile) +const isUniformFormat = (format: string): format is OpenId4VciCredentialFormatProfile => { + return Object.values(OpenId4VciCredentialFormatProfile).includes(format as OpenId4VciCredentialFormatProfile) } export function getUniformFormat( - format: string | OpenIdCredentialFormatProfile | CredentialFormat -): OpenIdCredentialFormatProfile { + format: string | OpenId4VciCredentialFormatProfile | CredentialFormat +): OpenId4VciCredentialFormatProfile { // Already valid format if (isUniformFormat(format)) return format // Older formats if (format === 'jwt_vc' || format === 'jwt') { - return OpenIdCredentialFormatProfile.JwtVcJson + return OpenId4VciCredentialFormatProfile.JwtVcJson } if (format === 'ldp_vc' || format === 'ldp') { - return OpenIdCredentialFormatProfile.LdpVc + return OpenId4VciCredentialFormatProfile.LdpVc } throw new AriesFrameworkError(`Invalid format: ${format}`) diff --git a/packages/openid4vc/src/openid4vc-holder/reception/utils/IssuerMetadataUtils.ts b/packages/openid4vc/src/openid4vc-holder/reception/utils/IssuerMetadataUtils.ts index 511ac4fab0..f6e08e1e31 100644 --- a/packages/openid4vc/src/openid4vc-holder/reception/utils/IssuerMetadataUtils.ts +++ b/packages/openid4vc/src/openid4vc-holder/reception/utils/IssuerMetadataUtils.ts @@ -21,7 +21,7 @@ import { MetadataClient } from '@sphereon/oid4vci-client' import { OpenId4VCIVersion } from '@sphereon/oid4vci-common' import { getUniformFormat, getFormatForVersion } from './Formats' -import { OpenIdCredentialFormatProfile } from './claimFormatMapping' +import { OpenId4VciCredentialFormatProfile } from './claimFormatMapping' /** * The type of a credential offer entry. For each item in `credentials` array, the type MUST be one of the following: @@ -36,19 +36,19 @@ export enum OfferedCredentialType { export type InlineOfferedCredentialWithMetadata = | { offerType: OfferedCredentialType.InlineCredentialOffer - format: OpenIdCredentialFormatProfile.JwtVcJson + format: OpenId4VciCredentialFormatProfile.JwtVcJson credentialOffer: CredentialOfferFormatJwtVcJson types: string[] } | { offerType: OfferedCredentialType.InlineCredentialOffer - format: OpenIdCredentialFormatProfile.JwtVcJsonLd | OpenIdCredentialFormatProfile.LdpVc + format: OpenId4VciCredentialFormatProfile.JwtVcJsonLd | OpenId4VciCredentialFormatProfile.LdpVc credentialOffer: CredentialOfferFormatJwtVcJsonLdAndLdpVc types: string[] } | { offerType: OfferedCredentialType.InlineCredentialOffer - format: OpenIdCredentialFormatProfile.SdJwtVc + format: OpenId4VciCredentialFormatProfile.SdJwtVc credentialOffer: CredentialOfferFormatSdJwtVc types: string[] } @@ -56,19 +56,19 @@ export type InlineOfferedCredentialWithMetadata = export type ReferencedOfferedCredentialWithMetadata = | { offerType: OfferedCredentialType.CredentialSupported - format: OpenIdCredentialFormatProfile.JwtVcJson + format: OpenId4VciCredentialFormatProfile.JwtVcJson credentialSupported: CredentialSupportedJwtVcJson types: string[] } | { offerType: OfferedCredentialType.CredentialSupported - format: OpenIdCredentialFormatProfile.JwtVcJsonLd | OpenIdCredentialFormatProfile.LdpVc + format: OpenId4VciCredentialFormatProfile.JwtVcJsonLd | OpenId4VciCredentialFormatProfile.LdpVc credentialSupported: CredentialSupportedJwtVcJsonLdAndLdpVc types: string[] } | { offerType: OfferedCredentialType.CredentialSupported - format: OpenIdCredentialFormatProfile.SdJwtVc + format: OpenId4VciCredentialFormatProfile.SdJwtVc credentialSupported: CredentialSupportedSdJwtVc types: string[] } @@ -113,7 +113,7 @@ export function getOfferedCredentialsWithMetadata( offeredCredentialsWithMetadata.push({ offerType: OfferedCredentialType.CredentialSupported, credentialSupported: foundSupportedCredential, - format: OpenIdCredentialFormatProfile.SdJwtVc, + format: OpenId4VciCredentialFormatProfile.SdJwtVc, types: [foundSupportedCredential.vct], }) } else { @@ -225,17 +225,17 @@ export function credentialSupportedV8ToV11( let credentialSupported: CredentialSupported const v11Format = getUniformFormat(format) - if (v11Format === OpenIdCredentialFormatProfile.JwtVcJson) { + if (v11Format === OpenId4VciCredentialFormatProfile.JwtVcJson) { credentialSupported = { - format: OpenIdCredentialFormatProfile.JwtVcJson, + format: OpenId4VciCredentialFormatProfile.JwtVcJson, display: supportedV8.display, ...credentialSupportedV8, credentialSubject: supportedV8.claims, id, } } else if ( - v11Format === OpenIdCredentialFormatProfile.JwtVcJsonLd || - v11Format === OpenIdCredentialFormatProfile.LdpVc + v11Format === OpenId4VciCredentialFormatProfile.JwtVcJsonLd || + v11Format === OpenId4VciCredentialFormatProfile.LdpVc ) { credentialSupported = { format: v11Format, diff --git a/packages/openid4vc/src/openid4vc-holder/reception/utils/__tests__/claimFormatMapping.test.ts b/packages/openid4vc/src/openid4vc-holder/reception/utils/__tests__/claimFormatMapping.test.ts index 193c4cb544..cf01f81ada 100644 --- a/packages/openid4vc/src/openid4vc-holder/reception/utils/__tests__/claimFormatMapping.test.ts +++ b/packages/openid4vc/src/openid4vc-holder/reception/utils/__tests__/claimFormatMapping.test.ts @@ -3,17 +3,17 @@ import { AriesFrameworkError, ClaimFormat } from '@aries-framework/core' import { fromDifClaimFormatToOpenIdCredentialFormatProfile, fromOpenIdCredentialFormatProfileToDifClaimFormat, - OpenIdCredentialFormatProfile, + OpenId4VciCredentialFormatProfile, } from '../claimFormatMapping' describe('claimFormatMapping', () => { it('should convert from openid credential format profile to DIF claim format', () => { expect(fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.LdpVc)).toStrictEqual( - OpenIdCredentialFormatProfile.LdpVc + OpenId4VciCredentialFormatProfile.LdpVc ) expect(fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.JwtVc)).toStrictEqual( - OpenIdCredentialFormatProfile.JwtVcJson + OpenId4VciCredentialFormatProfile.JwtVcJson ) expect(() => fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.Jwt)).toThrow(AriesFrameworkError) @@ -26,15 +26,15 @@ describe('claimFormatMapping', () => { }) it('should convert from DIF claim format to openid credential format profile', () => { - expect(fromOpenIdCredentialFormatProfileToDifClaimFormat(OpenIdCredentialFormatProfile.JwtVcJson)).toStrictEqual( - ClaimFormat.JwtVc - ) + expect( + fromOpenIdCredentialFormatProfileToDifClaimFormat(OpenId4VciCredentialFormatProfile.JwtVcJson) + ).toStrictEqual(ClaimFormat.JwtVc) - expect(fromOpenIdCredentialFormatProfileToDifClaimFormat(OpenIdCredentialFormatProfile.JwtVcJsonLd)).toStrictEqual( - ClaimFormat.JwtVc - ) + expect( + fromOpenIdCredentialFormatProfileToDifClaimFormat(OpenId4VciCredentialFormatProfile.JwtVcJsonLd) + ).toStrictEqual(ClaimFormat.JwtVc) - expect(fromOpenIdCredentialFormatProfileToDifClaimFormat(OpenIdCredentialFormatProfile.LdpVc)).toStrictEqual( + expect(fromOpenIdCredentialFormatProfileToDifClaimFormat(OpenId4VciCredentialFormatProfile.LdpVc)).toStrictEqual( ClaimFormat.LdpVc ) }) diff --git a/packages/openid4vc/src/openid4vc-holder/reception/utils/claimFormatMapping.ts b/packages/openid4vc/src/openid4vc-holder/reception/utils/claimFormatMapping.ts index 26a59e46fc..0978af6cd5 100644 --- a/packages/openid4vc/src/openid4vc-holder/reception/utils/claimFormatMapping.ts +++ b/packages/openid4vc/src/openid4vc-holder/reception/utils/claimFormatMapping.ts @@ -1,6 +1,6 @@ import { AriesFrameworkError, ClaimFormat } from '@aries-framework/core' -export enum OpenIdCredentialFormatProfile { +export enum OpenId4VciCredentialFormatProfile { JwtVcJson = 'jwt_vc_json', JwtVcJsonLd = 'jwt_vc_json-ld', LdpVc = 'ldp_vc', @@ -9,12 +9,12 @@ export enum OpenIdCredentialFormatProfile { export const fromDifClaimFormatToOpenIdCredentialFormatProfile = ( claimFormat: ClaimFormat -): OpenIdCredentialFormatProfile => { +): OpenId4VciCredentialFormatProfile => { switch (claimFormat) { case ClaimFormat.JwtVc: - return OpenIdCredentialFormatProfile.JwtVcJson + return OpenId4VciCredentialFormatProfile.JwtVcJson case ClaimFormat.LdpVc: - return OpenIdCredentialFormatProfile.LdpVc + return OpenId4VciCredentialFormatProfile.LdpVc default: throw new AriesFrameworkError( `Unsupported DIF claim format, ${claimFormat}, to map to an openid credential format profile` @@ -23,14 +23,14 @@ export const fromDifClaimFormatToOpenIdCredentialFormatProfile = ( } export const fromOpenIdCredentialFormatProfileToDifClaimFormat = ( - openidCredentialFormatProfile: OpenIdCredentialFormatProfile + openidCredentialFormatProfile: OpenId4VciCredentialFormatProfile ): ClaimFormat => { switch (openidCredentialFormatProfile) { - case OpenIdCredentialFormatProfile.JwtVcJson: + case OpenId4VciCredentialFormatProfile.JwtVcJson: return ClaimFormat.JwtVc - case OpenIdCredentialFormatProfile.JwtVcJsonLd: + case OpenId4VciCredentialFormatProfile.JwtVcJsonLd: return ClaimFormat.JwtVc - case OpenIdCredentialFormatProfile.LdpVc: + case OpenId4VciCredentialFormatProfile.LdpVc: return ClaimFormat.LdpVc default: throw new AriesFrameworkError( diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts index 527a6af405..94c635da44 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts @@ -52,7 +52,7 @@ import { import { IssueStatus } from '@sphereon/oid4vci-common' import { VcIssuerBuilder } from '@sphereon/oid4vci-issuer' -import { OpenIdCredentialFormatProfile } from '../openid4vc-holder' +import { OpenId4VciCredentialFormatProfile } from '../openid4vc-holder' import { OfferedCredentialType, getOfferedCredentialsWithMetadata, @@ -66,9 +66,9 @@ import { OpenId4VcIssuerRepository } from './repository/OpenId4VcIssuerRepositor import { storeIssuerIdForContextCorrelationId } from './router/requestContext' const w3cOpenId4VcFormats = [ - OpenIdCredentialFormatProfile.JwtVcJson, - OpenIdCredentialFormatProfile.JwtVcJsonLd, - OpenIdCredentialFormatProfile.LdpVc, + OpenId4VciCredentialFormatProfile.JwtVcJson, + OpenId4VciCredentialFormatProfile.JwtVcJsonLd, + OpenId4VciCredentialFormatProfile.LdpVc, ] /** @@ -347,14 +347,14 @@ export class OpenId4VcIssuerService { return referencedOfferedCredentials.filter((offeredCredential) => { if (offeredCredential.format !== credentialRequest.format) return false - if (credentialRequest.format === OpenIdCredentialFormatProfile.JwtVcJson) { + if (credentialRequest.format === OpenId4VciCredentialFormatProfile.JwtVcJson) { return equalsIgnoreOrder(offeredCredential.types, credentialRequest.types) } else if ( - credentialRequest.format === OpenIdCredentialFormatProfile.JwtVcJsonLd || - credentialRequest.format === OpenIdCredentialFormatProfile.LdpVc + credentialRequest.format === OpenId4VciCredentialFormatProfile.JwtVcJsonLd || + credentialRequest.format === OpenId4VciCredentialFormatProfile.LdpVc ) { return equalsIgnoreOrder(offeredCredential.types, credentialRequest.credential_definition.types) - } else if (credentialRequest.format === OpenIdCredentialFormatProfile.SdJwtVc) { + } else if (credentialRequest.format === OpenId4VciCredentialFormatProfile.SdJwtVc) { return equalsIgnoreOrder(offeredCredential.types, [credentialRequest.vct]) } }) @@ -387,9 +387,9 @@ export class OpenId4VcIssuerService { if (!format) throw new AriesFrameworkError('Missing format. Cannot issue credential.') const formatMap: Record = { - [OpenIdCredentialFormatProfile.JwtVcJson]: ClaimFormat.JwtVc, - [OpenIdCredentialFormatProfile.JwtVcJsonLd]: ClaimFormat.JwtVc, - [OpenIdCredentialFormatProfile.LdpVc]: ClaimFormat.LdpVc, + [OpenId4VciCredentialFormatProfile.JwtVcJson]: ClaimFormat.JwtVc, + [OpenId4VciCredentialFormatProfile.JwtVcJsonLd]: ClaimFormat.JwtVc, + [OpenId4VciCredentialFormatProfile.LdpVc]: ClaimFormat.LdpVc, } const w3cServiceFormat = formatMap[format] @@ -502,7 +502,7 @@ export class OpenId4VcIssuerService { } if (isW3cSignCredentialOptions(signOptions)) { - if (!w3cOpenId4VcFormats.includes(credentialRequest.format as OpenIdCredentialFormatProfile)) { + if (!w3cOpenId4VcFormats.includes(credentialRequest.format as OpenId4VciCredentialFormatProfile)) { throw new AriesFrameworkError( `The credential to be issued does not match the request. Cannot issue a W3cCredential if the client expects a credential of format '${credentialRequest.format}'.` ) @@ -514,9 +514,9 @@ export class OpenId4VcIssuerService { signCallback: this.getW3cCredentialSigningCallback(agentContext, signOptions), } } else { - if (credentialRequest.format !== OpenIdCredentialFormatProfile.SdJwtVc) { + if (credentialRequest.format !== OpenId4VciCredentialFormatProfile.SdJwtVc) { throw new AriesFrameworkError( - `Invalid credential format. Expected '${OpenIdCredentialFormatProfile.SdJwtVc}', received '${credentialRequest.format}'.` + `Invalid credential format. Expected '${OpenId4VciCredentialFormatProfile.SdJwtVc}', received '${credentialRequest.format}'.` ) } if (credentialRequest.vct !== signOptions.payload.vct) { diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierService.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierService.ts index 345e06702b..11139b0306 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierService.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierService.ts @@ -16,6 +16,7 @@ import type { import type { NextFunction, Response, Router } from 'express' import { + joinUriParts, InjectionSymbols, Logger, W3cCredentialService, @@ -42,7 +43,7 @@ import { } from '@sphereon/did-auth-siop' import bodyParser from 'body-parser' -import { getRequestContext, getEndpointUrl, initializeAgentFromContext } from '../shared/router' +import { getRequestContext } from '../shared/router' import { generateRandomValues, getSupportedDidMethods, @@ -158,11 +159,10 @@ export class OpenId4VcVerifierService { const redirectUri = verificationEndpointUrl ?? - getEndpointUrl( - this.verifierMetadata.verifierBaseUrl, + joinUriParts(this.verifierMetadata.verifierBaseUrl, [ this.openId4VcVerifierModuleConfig.getBasePath(agentContext), - this.verifierMetadata.verificationEndpointPath - ) + this.verifierMetadata.verificationEndpointPath, + ]) // Check: audience must be set to the issuer with dynamic disc otherwise self-issued.me/v2. const builder = RP.builder() @@ -341,9 +341,8 @@ export class OpenId4VcVerifierService { // initialize the agent and set the request context router.use(async (req: VerificationRequest, _res: Response, next: NextFunction) => { - const agentContext = await initializeAgentFromContext( - initializationContext.contextCorrelationId, - this.agentContextProvider + const agentContext = await this.agentContextProvider.getAgentContextForContextCorrelationId( + initializationContext.contextCorrelationId ) req.requestContext = { @@ -361,7 +360,7 @@ export class OpenId4VcVerifierService { ...endpointConfig.verificationEndpointConfig, }) - const endPointUrl = getEndpointUrl(this.verifierMetadata.verifierBaseUrl, basePath, verificationEndpointPath) + const endPointUrl = joinUriParts(this.verifierMetadata.verifierBaseUrl, [basePath, verificationEndpointPath]) this.logger.info(`[OID4VP] Verification endpoint running at '${endPointUrl}'.`) } diff --git a/packages/openid4vc/tests/utilsVci.ts b/packages/openid4vc/tests/utilsVci.ts index f3f764a03f..798c9f76af 100644 --- a/packages/openid4vc/tests/utilsVci.ts +++ b/packages/openid4vc/tests/utilsVci.ts @@ -1,36 +1,36 @@ import type { OpenId4VciCredentialSupportedWithId } from '../src' -import { OpenIdCredentialFormatProfile } from '../src' +import { OpenId4VciCredentialFormatProfile } from '../src' export const openBadgeCredential: OpenId4VciCredentialSupportedWithId = { id: `/credentials/OpenBadgeCredential`, - format: OpenIdCredentialFormatProfile.JwtVcJson, + format: OpenId4VciCredentialFormatProfile.JwtVcJson, types: ['VerifiableCredential', 'OpenBadgeCredential'], } export const universityDegreeCredential: OpenId4VciCredentialSupportedWithId = { id: `/credentials/UniversityDegreeCredential`, - format: OpenIdCredentialFormatProfile.JwtVcJson, + format: OpenId4VciCredentialFormatProfile.JwtVcJson, types: ['VerifiableCredential', 'UniversityDegreeCredential'], } export const universityDegreeCredentialLd: OpenId4VciCredentialSupportedWithId = { id: `/credentials/UniversityDegreeCredentialLd`, - format: OpenIdCredentialFormatProfile.JwtVcJsonLd, + format: OpenId4VciCredentialFormatProfile.JwtVcJsonLd, types: ['VerifiableCredential', 'UniversityDegreeCredential'], '@context': ['context'], } export const universityDegreeCredentialSdJwt = { id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredentialSdJwt', - format: OpenIdCredentialFormatProfile.SdJwtVc, + format: OpenId4VciCredentialFormatProfile.SdJwtVc, vct: 'UniversityDegreeCredential', cryptographic_binding_methods_supported: ['did:key'], } satisfies OpenId4VciCredentialSupportedWithId export const universityDegreeCredentialSdJwt2 = { id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredentialSdJwt2', - format: OpenIdCredentialFormatProfile.SdJwtVc, + format: OpenId4VciCredentialFormatProfile.SdJwtVc, vct: 'UniversityDegreeCredential2', // FIXME: should this be dynamically generated? I think static is fine for now cryptographic_binding_methods_supported: ['jwk'], From 02177239353a6fe3096710b794def1d7f8a6efbc Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Tue, 16 Jan 2024 17:08:01 +0700 Subject: [PATCH 097/115] so much changes Signed-off-by: Timo Glastra --- package.json | 4 +- packages/askar/src/wallet/AskarWallet.ts | 9 - .../cheqd/src/anoncreds/utils/identifiers.ts | 19 +- packages/core/src/index.ts | 1 + .../DifPresentationExchangeService.ts | 23 +- .../data-integrity/SignatureSuiteRegistry.ts | 3 + .../W3cJsonLdCredentialService.ts | 10 - packages/openid4vc/package.json | 4 +- .../openid4vc-holder/OpenId4VcHolderApi.ts | 16 +- .../openid4vc-holder/OpenId4VcHolderModule.ts | 5 +- .../__tests__/openId4vc-holder-module.test.ts | 5 +- .../presentation/OpenId4VpHolderService.ts | 40 +- .../OpenId4VpHolderServiceOptions.ts | 4 +- .../PresentationExchangeService.ts | 475 ------------------ .../openid4vc-holder/presentation/index.ts | 2 - .../selection/PexCredentialSelection.ts | 296 ----------- .../presentation/selection/example.md | 66 --- .../presentation/selection/index.ts | 2 - .../presentation/selection/types.ts | 121 ----- .../OpenId4VciHolderServiceOptions.ts | 2 + .../openid4vc-issuer/OpenId4VcIssuerModule.ts | 2 +- .../OpenId4VcIssuerService.ts | 7 +- .../OpenId4VcIssuerServiceOptions.ts | 7 +- .../OpenId4VcVerifierModule.ts | 2 +- packages/openid4vc/src/shared/models/index.ts | 2 + packages/openid4vc/src/shared/utils.ts | 17 +- packages/openid4vc/tests/utilsVp.ts | 6 +- packages/sd-jwt-vc/package.json | 2 +- packages/sd-jwt-vc/src/SdJwtVcService.ts | 5 +- .../src/__tests__/sdjwtvc.fixtures.ts | 2 +- yarn.lock | 144 +++++- 31 files changed, 220 insertions(+), 1083 deletions(-) delete mode 100644 packages/openid4vc/src/openid4vc-holder/presentation/PresentationExchangeService.ts delete mode 100644 packages/openid4vc/src/openid4vc-holder/presentation/selection/PexCredentialSelection.ts delete mode 100644 packages/openid4vc/src/openid4vc-holder/presentation/selection/example.md delete mode 100644 packages/openid4vc/src/openid4vc-holder/presentation/selection/index.ts delete mode 100644 packages/openid4vc/src/openid4vc-holder/presentation/selection/types.ts diff --git a/package.json b/package.json index 7ade6a1bab..579c63816f 100644 --- a/package.json +++ b/package.json @@ -63,8 +63,8 @@ }, "resolutions": { "@types/node": "18.18.8", - "@sphereon/ssi-types": "^0.17.6-unstable.69", - "@sphereon/pex-models": "^2.1.2" + "@sphereon/ssi-types": "^0.18.0", + "@sd-jwt/core": "0.1.2-alpha.6" }, "engines": { "node": ">=18" diff --git a/packages/askar/src/wallet/AskarWallet.ts b/packages/askar/src/wallet/AskarWallet.ts index d284dbaf65..afe0b1fa6a 100644 --- a/packages/askar/src/wallet/AskarWallet.ts +++ b/packages/askar/src/wallet/AskarWallet.ts @@ -22,7 +22,6 @@ import { inject, injectable } from 'tsyringe' import { AskarErrorCode, isAskarError, keyDerivationMethodToStoreKeyMethod, uriFromWalletConfig } from '../utils' import { AskarBaseWallet } from './AskarBaseWallet' -import { AskarProfileWallet } from './AskarProfileWallet' /** * @todo: rename after 0.5.0, as we now have multiple types of AskarWallet @@ -87,14 +86,6 @@ export class AskarWallet extends AskarBaseWallet { await this.close() } - /** - * TODO: we can add this method, and add custom logic in the tenants module - * or we can try to register the store on the agent context - */ - public async getProfileWallet() { - return new AskarProfileWallet(this.store, this.logger, this.signingKeyProviderRegistry) - } - /** * @throws {WalletDuplicateError} if the wallet already exists * @throws {WalletError} if another error occurs diff --git a/packages/cheqd/src/anoncreds/utils/identifiers.ts b/packages/cheqd/src/anoncreds/utils/identifiers.ts index ff21b32065..5dd622787d 100644 --- a/packages/cheqd/src/anoncreds/utils/identifiers.ts +++ b/packages/cheqd/src/anoncreds/utils/identifiers.ts @@ -9,18 +9,23 @@ const IDENTIFIER = `((?:${ID_CHAR}*:)*(${ID_CHAR}+))` const PATH = `(/[^#?]*)?` const QUERY = `([?][^#]*)?` const VERSION_ID = `(.*?)` +const FRAGMENT = `([#].*)?` export const cheqdSdkAnonCredsRegistryIdentifierRegex = new RegExp( - `^did:cheqd:${NETWORK}:${IDENTIFIER}${PATH}${QUERY}$` + `^did:cheqd:${NETWORK}:${IDENTIFIER}${PATH}${QUERY}${FRAGMENT}$` ) -export const cheqdDidRegex = new RegExp(`^did:cheqd:${NETWORK}:${IDENTIFIER}${QUERY}$`) -export const cheqdDidVersionRegex = new RegExp(`^did:cheqd:${NETWORK}:${IDENTIFIER}/version/${VERSION_ID}${QUERY}$`) -export const cheqdDidVersionsRegex = new RegExp(`^did:cheqd:${NETWORK}:${IDENTIFIER}/versions${QUERY}$`) -export const cheqdDidMetadataRegex = new RegExp(`^did:cheqd:${NETWORK}:${IDENTIFIER}/metadata${QUERY}$`) -export const cheqdResourceRegex = new RegExp(`^did:cheqd:${NETWORK}:${IDENTIFIER}/resources/${IDENTIFIER}${QUERY}$`) +export const cheqdDidRegex = new RegExp(`^did:cheqd:${NETWORK}:${IDENTIFIER}${QUERY}${FRAGMENT}$`) +export const cheqdDidVersionRegex = new RegExp( + `^did:cheqd:${NETWORK}:${IDENTIFIER}/version/${VERSION_ID}${QUERY}${FRAGMENT}$` +) +export const cheqdDidVersionsRegex = new RegExp(`^did:cheqd:${NETWORK}:${IDENTIFIER}/versions${QUERY}${FRAGMENT}$`) +export const cheqdDidMetadataRegex = new RegExp(`^did:cheqd:${NETWORK}:${IDENTIFIER}/metadata${QUERY}${FRAGMENT}$`) +export const cheqdResourceRegex = new RegExp( + `^did:cheqd:${NETWORK}:${IDENTIFIER}/resources/${IDENTIFIER}${QUERY}${FRAGMENT}$` +) export const cheqdResourceMetadataRegex = new RegExp( - `^did:cheqd:${NETWORK}:${IDENTIFIER}/resources/${IDENTIFIER}/metadata${QUERY}` + `^did:cheqd:${NETWORK}:${IDENTIFIER}/resources/${IDENTIFIER}/metadata${QUERY}${FRAGMENT}` ) export type ParsedCheqdDid = ParsedDid & { network: string } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1bb7010de2..af3a0077b6 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -61,6 +61,7 @@ export * from './modules/oob' export * from './modules/dids' export * from './modules/vc' export * from './modules/cache' +export * from './modules/dif-presentation-exchange' export { JsonEncoder, JsonTransformer, diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts index eab8642230..653a6bbaf9 100644 --- a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts @@ -40,6 +40,9 @@ import { export type ProofStructure = Record>> +/** + * @todo create a public api for using dif presentation exchange + */ @injectable() export class DifPresentationExchangeService { private pex = new PEX() @@ -445,29 +448,35 @@ export class DifPresentationExchangeService { // For each of the supported algs, find the key types, then find the proof types const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) - const supportedSignatureSuite = signatureSuiteRegistry.getByVerificationMethodType(verificationMethod.type) - if (!supportedSignatureSuite) { + const key = getKeyFromVerificationMethod(verificationMethod) + const supportedSignatureSuites = signatureSuiteRegistry.getByKeyType(key.keyType) + if (supportedSignatureSuites.length === 0) { throw new DifPresentationExchangeError( - `Couldn't find a supported signature suite for the given verification method type '${verificationMethod.type}'` + `Couldn't find a supported signature suite for the given key type '${key.keyType}'` ) } if (suitableSignatureSuites) { - if (suitableSignatureSuites.includes(supportedSignatureSuite.proofType) === false) { + const foundSignatureSuite = supportedSignatureSuites.find((suite) => + suitableSignatureSuites.includes(suite.proofType) + ) + + if (!foundSignatureSuite) { throw new DifPresentationExchangeError( [ 'No possible signature suite found for the given verification method.', `Verification method type: ${verificationMethod.type}`, - `SupportedSignatureSuite '${supportedSignatureSuite.proofType}'`, + `Key type: ${key.keyType}`, + `SupportedSignatureSuites: '${supportedSignatureSuites.map((s) => s.proofType).join(', ')}'`, `SuitableSignatureSuites: ${suitableSignatureSuites.join(', ')}`, ].join('\n') ) } - return supportedSignatureSuite.proofType + return supportedSignatureSuites[0].proofType } - return supportedSignatureSuite.proofType + return supportedSignatureSuites[0].proofType } public getPresentationSignCallback( diff --git a/packages/core/src/modules/vc/data-integrity/SignatureSuiteRegistry.ts b/packages/core/src/modules/vc/data-integrity/SignatureSuiteRegistry.ts index 68a4d5c01e..815dc961e6 100644 --- a/packages/core/src/modules/vc/data-integrity/SignatureSuiteRegistry.ts +++ b/packages/core/src/modules/vc/data-integrity/SignatureSuiteRegistry.ts @@ -27,6 +27,9 @@ export class SignatureSuiteRegistry { return this.suiteMapping.map((x) => x.proofType) } + /** + * @deprecated recommended to always search by key type instead as that will have broader support + */ public getByVerificationMethodType(verificationMethodType: string) { return this.suiteMapping.find((x) => x.verificationMethodTypes.includes(verificationMethodType)) } diff --git a/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts b/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts index 841271fa39..d2b347a7e2 100644 --- a/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts +++ b/packages/core/src/modules/vc/data-integrity/W3cJsonLdCredentialService.ts @@ -318,16 +318,6 @@ export class W3cJsonLdCredentialService { return this.signatureSuiteRegistry.getByProofType(proofType).keyTypes } - public getProofTypeByVerificationMethodType(verificationMethodType: string): string { - const suite = this.signatureSuiteRegistry.getByVerificationMethodType(verificationMethodType) - - if (!suite) { - throw new AriesFrameworkError(`No suite found for verification method type ${verificationMethodType}}`) - } - - return suite.proofType - } - public async getExpandedTypesForCredential(agentContext: AgentContext, credential: W3cJsonLdVerifiableCredential) { // Get the expanded types const expandedTypes: SingleOrArray = ( diff --git a/packages/openid4vc/package.json b/packages/openid4vc/package.json index a128b55880..3f2a00c8e6 100644 --- a/packages/openid4vc/package.json +++ b/packages/openid4vc/package.json @@ -26,10 +26,12 @@ "dependencies": { "@aries-framework/askar": "^0.4.2", "@aries-framework/core": "0.4.2", - "@sphereon/ssi-types": "^0.17.6-unstable.69", + "@sphereon/ssi-types": "^0.18.0", "@sphereon/oid4vci-client": "0.8.2-next.26", "@sphereon/oid4vci-common": "0.8.2-next.26", "@sphereon/oid4vci-issuer": "0.8.2-next.26", + "@sphereon/did-auth-siop": "0.6.0-unstable.0", + "@sphereon/pex": "^3.0.0", "@sphereon/pex-models": "^2.1.2", "body-parser": "^1.20.2", "jsonpath": "^1.1.1" diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts index 79fc119e3a..6f0f00cdfe 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts @@ -1,4 +1,4 @@ -import type { AuthenticationRequest, PresentationRequest, PresentationSubmission } from './presentation' +import type { AuthenticationRequest, PresentationRequest } from './presentation' import type { ResolvedCredentialOffer, ResolvedAuthorizationRequest, @@ -6,7 +6,7 @@ import type { AcceptCredentialOfferOptions, CredentialOfferPayloadV1_0_11, } from './reception' -import type { VerificationMethod } from '@aries-framework/core' +import type { VerificationMethod, DifPexInputDescriptorToCredentials } from '@aries-framework/core' import { injectable, AgentContext } from '@aries-framework/core' @@ -74,17 +74,11 @@ export class OpenId4VcHolderApi { * @returns @see ProofSubmissionResponse containing the status of the submission. */ public async acceptPresentationRequest( + // FIXME: more unique interface names: OpenId4VpPresentationRequest presentationRequest: PresentationRequest, - presentation: { - submission: PresentationSubmission - submissionEntryIndexes: number[] - } + credentials: DifPexInputDescriptorToCredentials ) { - const { submission, submissionEntryIndexes } = presentation - return await this.openId4VpHolderService.acceptProofRequest(this.agentContext, presentationRequest, { - submission, - submissionEntryIndexes, - }) + return await this.openId4VpHolderService.acceptProofRequest(this.agentContext, presentationRequest, credentials) } /** diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderModule.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderModule.ts index a141f8b74f..f8ff96e115 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderModule.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderModule.ts @@ -3,7 +3,7 @@ import type { DependencyManager, Module } from '@aries-framework/core' import { AgentConfig } from '@aries-framework/core' import { OpenId4VcHolderApi } from './OpenId4VcHolderApi' -import { OpenId4VpHolderService, PresentationExchangeService } from './presentation' +import { OpenId4VpHolderService } from './presentation' import { OpenId4VciHolderService } from './reception' /** @@ -21,7 +21,7 @@ export class OpenId4VcHolderModule implements Module { dependencyManager .resolve(AgentConfig) .logger.warn( - "The '@aries-framework/openid4vc-holder' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages. Multi-Tenancy is not supported." + "The '@aries-framework/openid4vc' Holder module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." ) // Api @@ -30,6 +30,5 @@ export class OpenId4VcHolderModule implements Module { // Services dependencyManager.registerSingleton(OpenId4VciHolderService) dependencyManager.registerSingleton(OpenId4VpHolderService) - dependencyManager.registerSingleton(PresentationExchangeService) } } diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/openId4vc-holder-module.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/openId4vc-holder-module.test.ts index 54f4f83eb6..a160f1a7e3 100644 --- a/packages/openid4vc/src/openid4vc-holder/__tests__/openId4vc-holder-module.test.ts +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/openId4vc-holder-module.test.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/unbound-method */ import type { DependencyManager } from '@aries-framework/core' -import { OpenId4VciHolderService, OpenId4VpHolderService, PresentationExchangeService } from '..' +import { OpenId4VciHolderService, OpenId4VpHolderService } from '..' import { OpenId4VcHolderApi } from '../OpenId4VcHolderApi' import { OpenId4VcHolderModule } from '../OpenId4VcHolderModule' @@ -20,9 +20,8 @@ describe('OpenId4VcHolderModule', () => { expect(dependencyManager.registerContextScoped).toHaveBeenCalledTimes(1) expect(dependencyManager.registerContextScoped).toHaveBeenCalledWith(OpenId4VcHolderApi) - expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(3) + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(2) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VciHolderService) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VpHolderService) - expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(PresentationExchangeService) }) }) diff --git a/packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderService.ts b/packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderService.ts index dd67abc623..9db95de7fd 100644 --- a/packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderService.ts @@ -1,6 +1,9 @@ -import type { PresentationSubmission } from './selection' -import type { InputDescriptorToCredentials } from './selection/types' -import type { AgentContext, VerificationMethod, W3cVerifiablePresentation } from '@aries-framework/core' +import type { + AgentContext, + DifPexInputDescriptorToCredentials, + VerificationMethod, + W3cVerifiablePresentation, +} from '@aries-framework/core' import type { W3CVerifiablePresentation } from '@sphereon/ssi-types' import { @@ -13,6 +16,7 @@ import { InjectionSymbols, Logger, parseDid, + DifPresentationExchangeService, } from '@aries-framework/core' import { CheckLinkedDomain, @@ -33,14 +37,14 @@ import { type ProofSubmissionResponse, type ResolvedProofRequest, } from './OpenId4VpHolderServiceOptions' -import { PresentationExchangeService } from './PresentationExchangeService' @injectable() export class OpenId4VpHolderService { private logger: Logger + public constructor( @inject(InjectionSymbols.Logger) logger: Logger, - private presentationExchangeService: PresentationExchangeService + private presentationExchangeService: DifPresentationExchangeService ) { this.logger = logger } @@ -116,12 +120,12 @@ export class OpenId4VpHolderService { ) } - const presentationSubmission = await this.presentationExchangeService.selectCredentialsForRequest( + const credentialsForRequest = await this.presentationExchangeService.getCredentialsForRequest( agentContext, verifiedAuthorizationRequest.presentationDefinitions[0].definition ) - return { proofType: 'presentation', presentationRequest: verifiedAuthorizationRequest, presentationSubmission } + return { proofType: 'presentation', presentationRequest: verifiedAuthorizationRequest, credentialsForRequest } } /** @@ -178,28 +182,8 @@ export class OpenId4VpHolderService { public async acceptProofRequest( agentContext: AgentContext, presentationRequest: PresentationRequest, - options: { - submission: PresentationSubmission - submissionEntryIndexes: number[] - } + credentialsForInputDescriptor: DifPexInputDescriptorToCredentials ): Promise { - const { submission, submissionEntryIndexes } = options - - const credentialsForInputDescriptor: InputDescriptorToCredentials = {} - - submission.requirements - .flatMap((requirement) => requirement.submissionEntry) - .forEach((submissionEntry, index) => { - const verifiableCredential = submissionEntry.verifiableCredentials[submissionEntryIndexes[index]] - - const inputDescriptor = credentialsForInputDescriptor[submissionEntry.inputDescriptorId] - if (!inputDescriptor) { - credentialsForInputDescriptor[submissionEntry.inputDescriptorId] = [verifiableCredential.credential] - } else { - inputDescriptor.push(verifiableCredential.credential) - } - }) - const { verifiablePresentations, presentationSubmission } = await this.presentationExchangeService.createPresentation(agentContext, { credentialsForInputDescriptor, diff --git a/packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderServiceOptions.ts b/packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderServiceOptions.ts index 7a59caf08b..11ab21b1d5 100644 --- a/packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderServiceOptions.ts @@ -1,4 +1,4 @@ -import type { PresentationSubmission } from './selection' +import type { DifPexCredentialsForRequest } from '@aries-framework/core/src' import type { AuthorizationResponsePayload, PresentationDefinitionWithLocation, @@ -24,7 +24,7 @@ export function isVerifiedAuthorizationRequestWithPresentationDefinition( export type ResolvedPresentationRequest = { proofType: 'presentation' presentationRequest: PresentationRequest - presentationSubmission: PresentationSubmission + credentialsForRequest: DifPexCredentialsForRequest } export type ResolvedAuthenticationRequest = { diff --git a/packages/openid4vc/src/openid4vc-holder/presentation/PresentationExchangeService.ts b/packages/openid4vc/src/openid4vc-holder/presentation/PresentationExchangeService.ts deleted file mode 100644 index a60d376895..0000000000 --- a/packages/openid4vc/src/openid4vc-holder/presentation/PresentationExchangeService.ts +++ /dev/null @@ -1,475 +0,0 @@ -import type { VpFormat } from './OpenId4VpHolderServiceOptions' -import type { InputDescriptorToCredentials, PresentationSubmission } from './selection/types' -import type { - AgentContext, - Query, - VerificationMethod, - W3cCredentialRecord, - W3cVerifiableCredential, - W3cVerifiablePresentation, -} from '@aries-framework/core' -import type { - IPresentationDefinition, - PresentationSignCallBackParams, - VerifiablePresentationResult, -} from '@sphereon/pex' -import type { - InputDescriptorV2, - PresentationSubmission as PexPresentationSubmission, - PresentationDefinitionV1, -} from '@sphereon/pex-models' -import type { OriginalVerifiableCredential } from '@sphereon/ssi-types' - -import { - AriesFrameworkError, - ClaimFormat, - DidsApi, - JsonTransformer, - SignatureSuiteRegistry, - W3cCredentialRepository, - W3cCredentialService, - W3cPresentation, - getJwkFromKey, - getKeyFromVerificationMethod, - injectable, -} from '@aries-framework/core' -import { PEVersion, PEX, PresentationSubmissionLocation } from '@sphereon/pex' - -import { - getSphereonOriginalVerifiableCredential, - getSphereonW3cVerifiablePresentation, - getW3cVerifiablePresentationInstance, -} from '../../shared/transform' - -import { selectCredentialsForRequest } from './selection/PexCredentialSelection' - -type ProofStructure = { - [subjectId: string]: { - [inputDescriptorId: string]: W3cVerifiableCredential[] - } -} - -@injectable() -export class PresentationExchangeService { - private pex = new PEX() - - public async selectCredentialsForRequest( - agentContext: AgentContext, - presentationDefinition: IPresentationDefinition - ): Promise { - const credentialRecords = await this.queryCredentialForPresentationDefinition(agentContext, presentationDefinition) - - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - const didRecords = await didsApi.getCreatedDids() - const holderDIDs = didRecords.map((didRecord) => didRecord.did) - - return selectCredentialsForRequest(presentationDefinition, credentialRecords, holderDIDs) - } - - /** - * Queries the wallet for credentials that match the given presentation definition. This only does an initial query based on the - * schema of the input descriptors. It does not do any further filtering based on the constraints in the input descriptors. - */ - private async queryCredentialForPresentationDefinition( - agentContext: AgentContext, - presentationDefinition: IPresentationDefinition - ) { - const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) - const query: Array> = [] - const presentationDefinitionVersion = PEX.definitionVersionDiscovery(presentationDefinition) - - if (!presentationDefinitionVersion.version) { - throw new AriesFrameworkError( - `Unable to determine the Presentation Exchange version from the presentation definition. ${ - presentationDefinitionVersion.error ?? 'Unknown error' - }` - ) - } - - if (presentationDefinitionVersion.version === PEVersion.v1) { - const pd = presentationDefinition as PresentationDefinitionV1 - - // The schema.uri can contain either an expanded type, or a context uri - for (const inputDescriptor of pd.input_descriptors) { - for (const schema of inputDescriptor.schema) { - query.push({ - $or: [{ expandedType: [schema.uri] }, { contexts: [schema.uri] }, { type: [schema.uri] }], - }) - } - } - } else if (presentationDefinitionVersion.version === PEVersion.v2) { - // FIXME: As PE version 2 does not have the `schema` anymore, we can't query by schema anymore. - // For now we retrieve ALL credentials, as we did the same for V1 with JWT credentials. We probably need - // to find some way to do initial filtering, hopefully if there's a filter on the `type` field or something. - } else { - throw new AriesFrameworkError( - `Unsupported presentation definition version ${presentationDefinitionVersion.version as unknown as string}` - ) - } - - // query the wallet ourselves first to avoid the need to query the pex library for all - // credentials for every proof request - const credentialRecords = await w3cCredentialRepository.findByQuery(agentContext, { $or: query }) - return credentialRecords - } - - private addCredentialToSubjectInputDescriptor( - subjectsToInputDescriptors: ProofStructure, - subjectId: string, - inputDescriptorId: string, - credential: W3cVerifiableCredential - ) { - const inputDescriptorsToCredentials = subjectsToInputDescriptors[subjectId] ?? {} - const credentials = inputDescriptorsToCredentials[inputDescriptorId] ?? [] - - credentials.push(credential) - inputDescriptorsToCredentials[inputDescriptorId] = credentials - subjectsToInputDescriptors[subjectId] = inputDescriptorsToCredentials - } - - private getPresentationFormat( - presentationDefinition: IPresentationDefinition, - credentials: OriginalVerifiableCredential[] - ): VpFormat { - const allCredentialsAreJwtVc = credentials?.every((c) => typeof c === 'string') - const allCredentialsAreLdpVc = credentials?.every((c) => typeof c !== 'string') - - const inputDescriptorsNotSupportingJwtVc = (presentationDefinition.input_descriptors as InputDescriptorV2[]).filter( - (d) => d.format && d.format.jwt_vc === undefined - ) - - const inputDescriptorsNotSupportingLdpVc = (presentationDefinition.input_descriptors as InputDescriptorV2[]).filter( - (d) => d.format && d.format.ldp_vc === undefined - ) - - if ( - allCredentialsAreJwtVc && - (presentationDefinition.format === undefined || presentationDefinition.format.jwt_vc) && - inputDescriptorsNotSupportingJwtVc.length === 0 - ) { - return 'jwt_vp' - } else if ( - allCredentialsAreLdpVc && - (presentationDefinition.format === undefined || presentationDefinition.format.ldp_vc) && - inputDescriptorsNotSupportingLdpVc.length === 0 - ) { - return 'ldp_vp' - } else { - throw new AriesFrameworkError( - 'No suitable presentation format found for the given presentation definition, and credentials' - ) - } - } - - public async createPresentation( - agentContext: AgentContext, - options: { - credentialsForInputDescriptor: InputDescriptorToCredentials - presentationDefinition: IPresentationDefinition - challenge?: string - domain?: string - nonce?: string - } - ) { - const { presentationDefinition, challenge, nonce, domain } = options - - const proofStructure: ProofStructure = {} - - Object.entries(options.credentialsForInputDescriptor).forEach(([inputDescriptorId, credentials]) => { - credentials.forEach((credential) => { - const subjectId = credential.credentialSubjectIds[0] - if (!subjectId) { - throw new AriesFrameworkError('Missing required credential subject for creating the presentation.') - } - - this.addCredentialToSubjectInputDescriptor(proofStructure, subjectId, inputDescriptorId, credential) - }) - }) - - const verifiablePresentationResultsWithFormat: { - verifiablePresentationResult: VerifiablePresentationResult - format: VpFormat - }[] = [] - - const subjectToInputDescriptors = Object.entries(proofStructure) - for (const [subjectId, subjectInputDescriptorsToCredentials] of subjectToInputDescriptors) { - // Determine a suitable verification method for the presentation - const verificationMethod = await this.getVerificationMethodForSubjectId(agentContext, subjectId) - - if (!verificationMethod) { - throw new AriesFrameworkError(`No verification method found for subject id '${subjectId}'.`) - } - - // We create a presentation for each subject - // Thus for each subject we need to filter all the related input descriptors and credentials - // FIXME: cast to V1, as tsc errors for strange reasons if not - const inputDescriptorsForSubject = (presentationDefinition as PresentationDefinitionV1).input_descriptors.filter( - (inputDescriptor) => inputDescriptor.id in subjectInputDescriptorsToCredentials - ) - - // Get all the credentials associated with the input descriptors - const credentialsForSubject = Object.values(subjectInputDescriptorsToCredentials) - .flatMap((credentials) => credentials) - .map(getSphereonOriginalVerifiableCredential) - - const presentationDefinitionForSubject: IPresentationDefinition = { - ...presentationDefinition, - input_descriptors: inputDescriptorsForSubject, - - // We remove the submission requirements, as it will otherwise fail to create the VP - submission_requirements: undefined, - } - - const format = this.getPresentationFormat(presentationDefinitionForSubject, credentialsForSubject) - - // FIXME: Q1: is holder always subject id, what if there are multiple subjects??? - // FIXME: Q2: What about proofType, proofPurpose verification method for multiple subjects? - const verifiablePresentationResult = await this.pex.verifiablePresentationFrom( - presentationDefinitionForSubject, - credentialsForSubject, - this.getPresentationSignCallback(agentContext, verificationMethod, format), - { - holderDID: subjectId, - proofOptions: { challenge, domain, nonce }, - signatureOptions: { verificationMethod: verificationMethod?.id }, - presentationSubmissionLocation: PresentationSubmissionLocation.EXTERNAL, - } - ) - - verifiablePresentationResultsWithFormat.push({ verifiablePresentationResult, format }) - } - - if (!verifiablePresentationResultsWithFormat[0]) { - throw new AriesFrameworkError('No verifiable presentations created.') - } - - if (!verifiablePresentationResultsWithFormat[0]) { - throw new AriesFrameworkError('No verifiable presentations created.') - } - - if (subjectToInputDescriptors.length !== verifiablePresentationResultsWithFormat.length) { - throw new AriesFrameworkError('Invalid amount of verifiable presentations created.') - } - - verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission - const presentationSubmission: PexPresentationSubmission = { - id: verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.id, - definition_id: - verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.definition_id, - descriptor_map: [], - } - - for (const vpf of verifiablePresentationResultsWithFormat) { - const { verifiablePresentationResult } = vpf - presentationSubmission.descriptor_map.push(...verifiablePresentationResult.presentationSubmission.descriptor_map) - } - - return { - verifiablePresentations: verifiablePresentationResultsWithFormat.map((r) => - getW3cVerifiablePresentationInstance(r.verifiablePresentationResult.verifiablePresentation) - ), - presentationSubmission, - presentationSubmissionLocation: - verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmissionLocation, - } - } - - private getSigningAlgorithmFromVerificationMethod( - verificationMethod: VerificationMethod, - suitableAlgorithms?: string[] - ) { - const key = getKeyFromVerificationMethod(verificationMethod) - const jwk = getJwkFromKey(key) - - if (suitableAlgorithms) { - const possibleAlgorithms = jwk.supportedSignatureAlgorithms.filter((alg) => suitableAlgorithms?.includes(alg)) - if (!possibleAlgorithms || possibleAlgorithms.length === 0) { - throw new AriesFrameworkError( - [ - `Found no suitable signing algorithm.`, - `Algorithms supported by Verification method: ${jwk.supportedSignatureAlgorithms.join(', ')}`, - `Suitable algorithms: ${suitableAlgorithms.join(', ')}`, - ].join('\n') - ) - } - } - - const alg = jwk.supportedSignatureAlgorithms[0] - if (!alg) throw new AriesFrameworkError(`No supported algs for key type: ${key.keyType}`) - return alg - } - - private getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( - algorithmsSatisfyingDefinition: string[], - inputDescriptorAlgorithms: string[][] - ) { - const allDescriptorAlgorithms = inputDescriptorAlgorithms.flat() - const algorithmsSatisfyingDescriptors = allDescriptorAlgorithms.filter((alg) => - inputDescriptorAlgorithms.every((descriptorAlgorithmSet) => descriptorAlgorithmSet.includes(alg)) - ) - - const algorithmsSatisfyingPdAndDescriptorRestrictions = algorithmsSatisfyingDefinition.filter((alg) => - algorithmsSatisfyingDescriptors.includes(alg) - ) - - if ( - algorithmsSatisfyingDefinition.length > 0 && - algorithmsSatisfyingDescriptors.length > 0 && - algorithmsSatisfyingPdAndDescriptorRestrictions.length === 0 - ) { - throw new AriesFrameworkError( - `No signature algorithm found for satisfying restrictions of the presentation definition and input descriptors.` - ) - } - - if (allDescriptorAlgorithms.length > 0 && algorithmsSatisfyingDescriptors.length === 0) { - throw new AriesFrameworkError( - `No signature algorithm found for satisfying restrictions of the input descriptors.` - ) - } - - let suitableAlgorithms: string[] | undefined = undefined - if (algorithmsSatisfyingPdAndDescriptorRestrictions.length > 0) { - suitableAlgorithms = algorithmsSatisfyingPdAndDescriptorRestrictions - } else if (algorithmsSatisfyingDescriptors.length > 0) { - suitableAlgorithms = algorithmsSatisfyingDescriptors - } else if (algorithmsSatisfyingDefinition.length > 0) { - suitableAlgorithms = algorithmsSatisfyingDefinition - } - - return suitableAlgorithms - } - - private getSigningAlgorithmForJwtVc( - presentationDefinition: IPresentationDefinition, - verificationMethod: VerificationMethod - ) { - const algorithmsSatisfyingDefinition = presentationDefinition.format?.jwt_vc?.alg || [] - - const inputDescriptorAlgorithms: string[][] = presentationDefinition.input_descriptors - .map((descriptor) => (descriptor as InputDescriptorV2).format?.jwt_vc?.alg || []) - .filter((alg) => alg.length > 0) - - const suitableAlgorithms = this.getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( - algorithmsSatisfyingDefinition, - inputDescriptorAlgorithms - ) - - return this.getSigningAlgorithmFromVerificationMethod(verificationMethod, suitableAlgorithms) - } - - private getProofTypeForLdpVc( - agentContext: AgentContext, - presentationDefinition: IPresentationDefinition, - verificationMethod: VerificationMethod - ) { - const algorithmsSatisfyingDefinition = presentationDefinition.format?.ldp_vc?.proof_type || [] - - const inputDescriptorAlgorithms: string[][] = presentationDefinition.input_descriptors - .map((descriptor) => (descriptor as InputDescriptorV2).format?.ldp_vc?.proof_type || []) - .filter((alg) => alg.length > 0) - - const suitableSignatureSuites = this.getSigningAlgorithmsForPresentationDefinitionAndInputDescriptors( - algorithmsSatisfyingDefinition, - inputDescriptorAlgorithms - ) - - // For each of the supported algs, find the key types, then find the proof types - const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) - - const supportedSignatureSuite = signatureSuiteRegistry.getByVerificationMethodType(verificationMethod.type) - if (!supportedSignatureSuite) { - throw new AriesFrameworkError( - `Couldn't find a supported signature suite for the given verification method type '${verificationMethod.type}'.` - ) - } - - if (suitableSignatureSuites && suitableSignatureSuites.includes(supportedSignatureSuite.proofType) === false) { - throw new AriesFrameworkError( - [ - 'No possible signature suite found for the given verification method.', - `Verification method type: ${verificationMethod.type}`, - `SupportedSignatureSuite '${supportedSignatureSuite.proofType}'`, - `SuitableSignatureSuites: ${suitableSignatureSuites.join(', ')}`, - ].join('\n') - ) - } - - return supportedSignatureSuite.proofType - } - - public getPresentationSignCallback( - agentContext: AgentContext, - verificationMethod: VerificationMethod, - vpFormat: VpFormat - ) { - const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) - - return async (callBackParams: PresentationSignCallBackParams) => { - // The created partial proof and presentation, as well as original supplied options - const { presentation: presentationJson, options, presentationDefinition } = callBackParams - const { challenge, domain, nonce } = options.proofOptions ?? {} - const { verificationMethod: verificationMethodId } = options.signatureOptions ?? {} - - if (verificationMethodId && verificationMethodId !== verificationMethod.id) { - throw new AriesFrameworkError( - `Verification method from signing options ${verificationMethodId} does not match verification method ${verificationMethod.id}.` - ) - } - - // Clients MUST ignore any presentation_submission element included inside a Verifiable Presentation. - delete presentationJson.presentation_submission - - let signedPresentation: W3cVerifiablePresentation - if (vpFormat === 'jwt_vp') { - signedPresentation = await w3cCredentialService.signPresentation(agentContext, { - format: ClaimFormat.JwtVp, - alg: this.getSigningAlgorithmForJwtVc(presentationDefinition, verificationMethod), - verificationMethod: verificationMethod.id, - presentation: JsonTransformer.fromJSON(presentationJson, W3cPresentation), - challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), - domain, - }) - } else if (vpFormat === 'ldp_vp') { - signedPresentation = await w3cCredentialService.signPresentation(agentContext, { - format: ClaimFormat.LdpVp, - proofType: this.getProofTypeForLdpVc(agentContext, presentationDefinition, verificationMethod), - proofPurpose: 'authentication', - verificationMethod: verificationMethod.id, - presentation: JsonTransformer.fromJSON(presentationJson, W3cPresentation), - challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), - domain, - }) - } else { - throw new AriesFrameworkError( - `Only JWT credentials or JSONLD credentials are supported for a single presentation.` - ) - } - - return getSphereonW3cVerifiablePresentation(signedPresentation) - } - } - - private async getVerificationMethodForSubjectId(agentContext: AgentContext, subjectId: string) { - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - - if (!subjectId.startsWith('did:')) { - throw new AriesFrameworkError(`Only dids are supported as credentialSubject id. ${subjectId} is not a valid did`) - } - - const didDocument = await didsApi.resolveDidDocument(subjectId) - - if (!didDocument.authentication || didDocument.authentication.length === 0) { - throw new AriesFrameworkError(`No authentication verificationMethods found for did ${subjectId} in did document`) - } - - // the signature suite to use for the presentation is dependant on the credentials we share. - // 1. Get the verification method for this given proof purpose in this DID document - let [verificationMethod] = didDocument.authentication - if (typeof verificationMethod === 'string') { - verificationMethod = didDocument.dereferenceKey(verificationMethod, ['authentication']) - } - - return verificationMethod - } -} diff --git a/packages/openid4vc/src/openid4vc-holder/presentation/index.ts b/packages/openid4vc/src/openid4vc-holder/presentation/index.ts index b2fa029010..ec4b86d2dc 100644 --- a/packages/openid4vc/src/openid4vc-holder/presentation/index.ts +++ b/packages/openid4vc/src/openid4vc-holder/presentation/index.ts @@ -1,4 +1,2 @@ export * from './OpenId4VpHolderService' export * from './OpenId4VpHolderServiceOptions' -export { PresentationExchangeService } from './PresentationExchangeService' -export { PresentationSubmission, SubmissionEntry } from './selection' diff --git a/packages/openid4vc/src/openid4vc-holder/presentation/selection/PexCredentialSelection.ts b/packages/openid4vc/src/openid4vc-holder/presentation/selection/PexCredentialSelection.ts deleted file mode 100644 index f51bcbf3bf..0000000000 --- a/packages/openid4vc/src/openid4vc-holder/presentation/selection/PexCredentialSelection.ts +++ /dev/null @@ -1,296 +0,0 @@ -import type { PresentationSubmission, PresentationSubmissionRequirement, SubmissionEntry } from './types' -import type { W3cCredentialRecord } from '@aries-framework/core' -import type { IPresentationDefinition, SelectResults, SubmissionRequirementMatch } from '@sphereon/pex' -import type { InputDescriptorV1, InputDescriptorV2, SubmissionRequirement } from '@sphereon/pex-models' - -import { AriesFrameworkError, deepEquality } from '@aries-framework/core' -import { PEX } from '@sphereon/pex' -import { Rules } from '@sphereon/pex-models' -import { default as jp } from 'jsonpath' - -import { getSphereonOriginalVerifiableCredential } from '../../../shared/transform' - -export async function selectCredentialsForRequest( - presentationDefinition: IPresentationDefinition, - credentialRecords: W3cCredentialRecord[], - holderDIDs: string[] -): Promise { - const sphereonEncodedCredentials = credentialRecords.map((c) => getSphereonOriginalVerifiableCredential(c.credential)) - - const pex = new PEX() - - // FIXME: there is a function for this in the VP library, but it is not usable atm - const selectResultsRaw = pex.selectFrom(presentationDefinition, sphereonEncodedCredentials, { - holderDIDs, - // limitDisclosureSignatureSuites: [], - // restrictToDIDMethods, - // restrictToFormats - }) - - const selectResults = { - ...selectResultsRaw, - // Map the encoded credential to their respective w3c credential record - verifiableCredential: selectResultsRaw.verifiableCredential?.map((encoded) => { - const credentialIndex = - typeof encoded === 'string' - ? sphereonEncodedCredentials.indexOf(encoded) - : sphereonEncodedCredentials.findIndex((sphereonEncoded) => deepEquality(encoded, sphereonEncoded)) - const credentialRecord = credentialRecords[credentialIndex] - if (!credentialRecord) throw new AriesFrameworkError('Unable to find credential in credential records.') - - return credentialRecord - }), - } - - const presentationSubmission: PresentationSubmission = { - requirements: [], - areRequirementsSatisfied: false, - name: presentationDefinition.name, - purpose: presentationDefinition.purpose, - } - - // If there's no submission requirements, ALL input descriptors MUST be satisfied - if (!presentationDefinition.submission_requirements || presentationDefinition.submission_requirements.length === 0) { - presentationSubmission.requirements = getSubmissionRequirementsForAllInputDescriptors( - presentationDefinition.input_descriptors, - selectResults - ) - } else { - presentationSubmission.requirements = getSubmissionRequirements(presentationDefinition, selectResults) - } - - // There may be no requirements if we filter out all optional ones. To not makes things too complicated, we see it as an error - // for now if a request is made that has no required requirements (but only e.g. min: 0, which means we don't need to disclose anything) - // I see this more as the fault of the presentation definition, as it should have at least some requirements. - if (presentationSubmission.requirements.length === 0) { - throw new AriesFrameworkError( - 'Presentation Definition does not require any credentials. Optional credentials are not included in the presentation submission.' - ) - } - if (selectResultsRaw.areRequiredCredentialsPresent === 'error') { - return presentationSubmission - } - - return { - ...presentationSubmission, - - // If all requirements are satisfied, the presentation submission is satisfied - areRequirementsSatisfied: presentationSubmission.requirements.every( - (requirement) => requirement.isRequirementSatisfied - ), - } -} - -function getSubmissionRequirements( - presentationDefinition: IPresentationDefinition, - selectResults: W3cCredentialRecordSelectResults -): PresentationSubmissionRequirement[] { - const submissionRequirements: PresentationSubmissionRequirement[] = [] - - // There are submission requirements, so we need to select the input_descriptors - // based on the submission requirements - for (const submissionRequirement of presentationDefinition.submission_requirements ?? []) { - // Check: if the submissionRequirement uses `from_nested`, as we don't support this yet - if (submissionRequirement.from_nested) { - throw new AriesFrameworkError( - "Presentation definition contains requirement using 'from_nested', which is not supported yet." - ) - } - - // Check if there's a 'from'. If not the structure is not as we expect it - if (!submissionRequirement.from) { - throw new AriesFrameworkError("Missing 'from' in submission requirement match") - } - - if (submissionRequirement.rule === Rules.All) { - const selectedSubmission = getSubmissionRequirementRuleAll( - submissionRequirement, - presentationDefinition, - selectResults - ) - submissionRequirements.push(selectedSubmission) - } else { - const selectedSubmission = getSubmissionRequirementRulePick( - submissionRequirement, - presentationDefinition, - selectResults - ) - - submissionRequirements.push(selectedSubmission) - } - } - - // Submission may have requirement that doesn't require a credential to be submitted (e.g. min: 0) - // We use minimization strategy, and thus only disclose the minimum amount of information - const requirementsWithCredentials = submissionRequirements.filter((requirement) => requirement.needsCount > 0) - - return requirementsWithCredentials -} - -function getSubmissionRequirementsForAllInputDescriptors( - inputDescriptors: InputDescriptorV1[] | InputDescriptorV2[], - selectResults: W3cCredentialRecordSelectResults -): PresentationSubmissionRequirement[] { - const submissionRequirements: PresentationSubmissionRequirement[] = [] - - for (const inputDescriptor of inputDescriptors) { - const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) - - submissionRequirements.push({ - rule: 'pick', - needsCount: 1, // Every input descriptor is a distinct requirement, so the count is always 1, - submissionEntry: [submission], - isRequirementSatisfied: submission.verifiableCredentials.length >= 1, - }) - } - - return submissionRequirements -} - -function getSubmissionRequirementRuleAll( - submissionRequirement: SubmissionRequirement, - presentationDefinition: IPresentationDefinition, - selectResults: W3cCredentialRecordSelectResults -) { - // Check if there's a 'from'. If not the structure is not as we expect it - if (!submissionRequirement.from) throw new AriesFrameworkError("Missing 'from' in submission requirement match.") - - const selectedSubmission: PresentationSubmissionRequirement = { - rule: 'all', - needsCount: 0, - name: submissionRequirement.name, - purpose: submissionRequirement.purpose, - submissionEntry: [], - isRequirementSatisfied: false, - } - - for (const inputDescriptor of presentationDefinition.input_descriptors) { - // We only want to get the submission if the input descriptor belongs to the group - if (!inputDescriptor.group?.includes(submissionRequirement.from)) continue - - const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) - - // Rule ALL, so for every input descriptor that matches in this group, we need to add it - selectedSubmission.needsCount += 1 - selectedSubmission.submissionEntry.push(submission) - } - - return { - ...selectedSubmission, - - // If all submissions have a credential, the requirement is satisfied - isRequirementSatisfied: selectedSubmission.submissionEntry.every( - (submission) => submission.verifiableCredentials.length >= 1 - ), - } -} - -function getSubmissionRequirementRulePick( - submissionRequirement: SubmissionRequirement, - presentationDefinition: IPresentationDefinition, - selectResults: W3cCredentialRecordSelectResults -) { - // Check if there's a 'from'. If not the structure is not as we expect it - if (!submissionRequirement.from) throw new AriesFrameworkError("Missing 'from' in submission requirement match.") - - const selectedSubmission: PresentationSubmissionRequirement = { - rule: 'pick', - needsCount: submissionRequirement.count ?? submissionRequirement.min ?? 1, - name: submissionRequirement.name, - purpose: submissionRequirement.purpose, - // If there's no count, min, or max we assume one credential is required for submission - // however, the exact behavior is not specified in the spec - submissionEntry: [], - isRequirementSatisfied: false, - } - - const satisfiedSubmissions: SubmissionEntry[] = [] - const unsatisfiedSubmissions: SubmissionEntry[] = [] - - for (const inputDescriptor of presentationDefinition.input_descriptors) { - // We only want to get the submission if the input descriptor belongs to the group - if (!inputDescriptor.group?.includes(submissionRequirement.from)) continue - - const submission = getSubmissionForInputDescriptor(inputDescriptor, selectResults) - - if (submission.verifiableCredentials.length >= 1) { - satisfiedSubmissions.push(submission) - } else { - unsatisfiedSubmissions.push(submission) - } - - // If we have found enough credentials to satisfy the requirement, we could stop - // but the user may not want the first x that match, so we continue and return all matches - // if (satisfiedSubmissions.length === selectedSubmission.needsCount) break - } - - return { - ...selectedSubmission, - - // If there are enough satisfied submissions, the requirement is satisfied - isRequirementSatisfied: satisfiedSubmissions.length >= selectedSubmission.needsCount, - - // if the requirement is satisfied, we only need to return the satisfied submissions - // however if the requirement is not satisfied, we include all entries so the wallet could - // render which credentials are missing. - submission: - satisfiedSubmissions.length >= selectedSubmission.needsCount - ? satisfiedSubmissions - : [...satisfiedSubmissions, ...unsatisfiedSubmissions], - } -} - -function getSubmissionForInputDescriptor( - inputDescriptor: InputDescriptorV1 | InputDescriptorV2, - selectResults: W3cCredentialRecordSelectResults -): SubmissionEntry { - // https://github.com/Sphereon-Opensource/PEX/issues/116 - // If the input descriptor doesn't contain a name, the name of the match will be the id of the input descriptor that satisfied it - const matchesForInputDescriptor = selectResults.matches?.filter( - (m) => - m.name === inputDescriptor.id || - // FIXME: this is not collision proof as the name doesn't have to be unique - m.name === inputDescriptor.name - ) - - const submissionEntry: SubmissionEntry = { - inputDescriptorId: inputDescriptor.id, - name: inputDescriptor.name, - purpose: inputDescriptor.purpose, - verifiableCredentials: [], - } - - // return early if no matches. - if (!matchesForInputDescriptor?.length) return submissionEntry - - // FIXME: This can return multiple credentials for multiple input_descriptors, - // which I think is a bug in the PEX library - // Extract all credentials from the match - const verifiableCredentials = matchesForInputDescriptor.flatMap((matchForInputDescriptor) => - extractCredentialsFromMatch(matchForInputDescriptor, selectResults.verifiableCredential) - ) - - submissionEntry.verifiableCredentials = verifiableCredentials - - return submissionEntry -} - -function extractCredentialsFromMatch(match: SubmissionRequirementMatch, availableCredentials?: W3cCredentialRecord[]) { - const verifiableCredentials: W3cCredentialRecord[] = [] - - for (const vcPath of match.vc_path) { - const [verifiableCredential] = jp.query({ verifiableCredential: availableCredentials }, vcPath) as [ - W3cCredentialRecord - ] - verifiableCredentials.push(verifiableCredential) - } - - return verifiableCredentials -} - -/** - * Custom SelectResults that include the W3cCredentialRecord instead of the encoded verifiable credential - */ -export type W3cCredentialRecordSelectResults = Omit & { - verifiableCredential?: W3cCredentialRecord[] -} diff --git a/packages/openid4vc/src/openid4vc-holder/presentation/selection/example.md b/packages/openid4vc/src/openid4vc-holder/presentation/selection/example.md deleted file mode 100644 index 2a0f170c2e..0000000000 --- a/packages/openid4vc/src/openid4vc-holder/presentation/selection/example.md +++ /dev/null @@ -1,66 +0,0 @@ -# Presentation Submission Example - -This document gives an example of the result returned by `PresentationExchangeService.selectCredentialsForRequest`. - -On startup of the agent if the wallet does not have a DBC credential yet, it will be added. In the `WalletScreen` I'v added an useEffect with a note that should how you can get the below example results for rendering. There's no way to submit yet, but this is enough to render everything for the proof request. - -### Request can be satisfied - -The following value represents a presentation that can be satisfied using the following submission. The value `areRequirementsSatisfied: true` indicates that all requirements are met. - -Each requirement can contain 1 to N `submissions` entries, where each submission contributes to the requirement. If `isRequirementSatisfied` is `true`, you can render all `submission` entries as a credential on the proof share page. - -Each requirement represents a different group (not sure if we want to show this as separate groups, but each group can have a `name` and `purpose`) - -```json -{ - "areRequirementsSatisfied": true, - "requirements": [ - { - "isRequirementSatisfied": true, - "needsCount": 1, - "submission": [ - { - "inputDescriptorId": "c2834d0e-3c95-4721-b21a-40e3d7ea2549", - "name": "DBC Conference 2023 Attendee", - "purpose": "To access this portal your DBC Conference 2023 attendance proof is required.", - "verifiableCredential": - } - ] - } - ], - "purpose": "We want to know your name and e-mail address (will not be stored)" -} -``` - -### Request could not be satisfied. - -The example does not satisfy the requirements. As you can see in `areRequirementsSatisfied: false`. If this is the case you need to loop through all the requirement and for each requirement determine whether the requirement is satisfied (`isRequirementSatisfied: true`). If this is the case you can render the submission entries as is with the succesfull case above. It there's a requirement that is not satisfied (`isRequirementSatisfied: false`), the submission entries will contain a list of submission that entries **that could satisfy the requirement**. However there will be entries where the `verifiableCredential` value is `undefined`. - -An example is a requirement that has `needsCount: 3`, but there's only 2 submission entries that could be satisfied. The `submission` list can have a length of 4 . In this case the verifier says: Here's 4 requirements, you can choose any 3 (indicated by `needsCount`) of these submission possiblities. If two submission entries could be satisfied, there will be a list of 4 submission entries, where 2 of them have a `verifiableCredential` value of `undefined`, and two will have a `verifiableCredential` value that can be rendered. This allows the wallet to say you have 2 credentials, but you are missing 1 of these two (and those could include the `name` and `purpose` of that submission so user knows what they need to get to satisfy this request). This is maybe overly complex for now, but so you have at least at the information that is available to show the user exactly why a presentation can't be satisfied. If you want you could also just check `areRequirementsSatisfied: true` and show a general error screen otherwise, but this gives the user less info about what went wrong. - -```jsonc -{ - "areRequirementsSatisfied": false, - "requirements": [ - { - "isRequirementSatisfied": false, - "submission": [ - { - "inputDescriptorId": "c2834d0e-3c95-4721-b21a-40e3d7ea2549", - "name": "DBC Conference 2023 Attendee", - "purpose": "To access this portal your DBC Conference 2023 attendance proof is required.", - "verifiableCredential": - }, - { - "inputDescriptorId": "c2834d0e-3c95-4721-b21a-40e3d7ea2549", - "name": "Not Present", - "purpose": "We want a credential you don't have" - } - ], - "needsCount": 2 - } - ], - "purpose": "We want to know your name and e-mail address (will not be stored)" -} -``` diff --git a/packages/openid4vc/src/openid4vc-holder/presentation/selection/index.ts b/packages/openid4vc/src/openid4vc-holder/presentation/selection/index.ts deleted file mode 100644 index 9c6bddae57..0000000000 --- a/packages/openid4vc/src/openid4vc-holder/presentation/selection/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { selectCredentialsForRequest } from './PexCredentialSelection' -export { PresentationSubmission, PresentationSubmissionRequirement, SubmissionEntry } from './types' diff --git a/packages/openid4vc/src/openid4vc-holder/presentation/selection/types.ts b/packages/openid4vc/src/openid4vc-holder/presentation/selection/types.ts deleted file mode 100644 index df0cd2e7f2..0000000000 --- a/packages/openid4vc/src/openid4vc-holder/presentation/selection/types.ts +++ /dev/null @@ -1,121 +0,0 @@ -import type { W3cCredentialRecord, W3cVerifiableCredential } from '@aries-framework/core' - -/** - * A submission entry that satisfies a specific input descriptor from the - * presentation definition. - */ -export interface SubmissionEntry { - /** - * The id of the input descriptor - */ - inputDescriptorId: string - - /** - * Name of the input descriptor - */ - name?: string - - /** - * Purpose of the input descriptor - */ - purpose?: string - - /** - * The verifiable credentials that satisfy the input descriptor. - * - * If the value is an empty list, it means the input descriptor could - * not be satisfied. - */ - verifiableCredentials: W3cCredentialRecord[] -} - -/** - * A requirement for the presentation submission. A requirement - * is a group of input descriptors that together fulfill a requirement - * from the presentation definition. - * - * Each submission represents a input descriptor. - */ -export interface PresentationSubmissionRequirement { - /** - * Whether the requirement is satisfied. - * - * If the requirement is not satisfied, the submission will still contain - * entries, but the `verifiableCredentials` list will be empty. - */ - isRequirementSatisfied: boolean - - /** - * Name of the requirement - */ - name?: string - - /** - * Purpose of the requirement - */ - purpose?: string - - /** - * Array of objects, where each entry contains a credential that will be part - * of the submission. - * - * NOTE: if the `isRequirementSatisfied` is `false` the submission list will - * contain entries where the verifiable credential list is empty. In this case it could also - * contain more entries than are actually needed (as you sometimes can choose from - * e.g. 4 types of credentials and need to submit at least two). If - * `isRequirementSatisfied` is `false`, make sure to check the `needsCount` value - * to see how many of those submissions needed. - */ - submissionEntry: SubmissionEntry[] - - /** - * The number of submission entries that are needed to fulfill the requirement. - * If `isRequirementSatisfied` is `true`, the submission list will always be equal - * to the number of `needsCount`. If `isRequirementSatisfied` is `false` the list of - * submissions could be longer. - */ - needsCount: number - - /** - * The rule that is used to select the credentials for the submission. - * If the rule is `pick`, the user can select which credentials to use for the submission. - * If the rule is `all`, all credentials that satisfy the input descriptor will be used. - */ - rule: 'pick' | 'all' -} - -export interface PresentationSubmission { - /** - * Whether all requirements have been satisfied by the credentials in the wallet. - */ - areRequirementsSatisfied: boolean - - /** - * The requirements for the presentation definition. If the `areRequirementsSatisfied` value - * is `false`, this list will still be populated with requirements, but won't contain credentials - * for all requirements. This can be useful to display the missing credentials for a presentation - * definition to be satisfied. - * - * NOTE: Presentation definition requirements can be really complex as there's a lot of different - * combinations that are possible. The structure doesn't include all possible combinations yet that - * could satisfy a presentation definition. - */ - requirements: PresentationSubmissionRequirement[] - - /** - * Name of the presentation definition - */ - name?: string - - /** - * Purpose of the presentation definition. - */ - purpose?: string -} - -/** - * Mapping of selected credentials for an input descriptor - */ -export interface InputDescriptorToCredentials { - [inputDescriptorId: string]: W3cVerifiableCredential[] -} diff --git a/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderServiceOptions.ts b/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderServiceOptions.ts index c04bff021e..b448d60964 100644 --- a/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderServiceOptions.ts @@ -14,11 +14,13 @@ export type SupportedCredentialFormats = | OpenId4VciCredentialFormatProfile.JwtVcJson | OpenId4VciCredentialFormatProfile.JwtVcJsonLd | OpenId4VciCredentialFormatProfile.SdJwtVc + | OpenId4VciCredentialFormatProfile.LdpVc export const supportedCredentialFormats: SupportedCredentialFormats[] = [ OpenId4VciCredentialFormatProfile.JwtVcJson, OpenId4VciCredentialFormatProfile.JwtVcJsonLd, OpenId4VciCredentialFormatProfile.SdJwtVc, + OpenId4VciCredentialFormatProfile.LdpVc, ] export type { OpenId4VCIVersion, EndpointMetadataResult, CredentialOfferPayloadV1_0_11, AuthorizationDetails } diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts index 0d0c3945f1..e3670876bc 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts @@ -33,7 +33,7 @@ export class OpenId4VcIssuerModule implements Module { dependencyManager .resolve(AgentConfig) .logger.warn( - "The '@aries-framework/openid4vc' Issuer module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages. Multi-Tenancy is not supported." + "The '@aries-framework/openid4vc' Issuer module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." ) // Register config diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts index 94c635da44..d4ec3c54f0 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts @@ -58,7 +58,7 @@ import { getOfferedCredentialsWithMetadata, } from '../openid4vc-holder/reception/utils/IssuerMetadataUtils' import { getSphereonW3cVerifiableCredential } from '../shared/transform' -import { getProofTypeFromVerificationMethod } from '../shared/utils' +import { getProofTypeFromKey } from '../shared/utils' import { OpenId4VcIssuerModuleConfig } from './OpenId4VcIssuerModuleConfig' import { OpenId4VcIssuerRecord } from './repository/OpenId4VcIssuerRecord' @@ -423,11 +423,14 @@ export class OpenId4VcIssuerService { return getSphereonW3cVerifiableCredential(signed) } else { + const key = getKeyFromVerificationMethod(verificationMethod) + const proofType = getProofTypeFromKey(agentContext, key) + const signed = await this.w3cCredentialService.signCredential(agentContext, { format: w3cServiceFormat, credential: options.credential, verificationMethod: options.verificationMethod, - proofType: getProofTypeFromVerificationMethod(agentContext, verificationMethod), + proofType: proofType, }) return getSphereonW3cVerifiableCredential(signed) diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts index 8883581fa5..d47eb147e9 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts @@ -4,10 +4,11 @@ import type { OpenId4VciCredentialOffer, OpenId4VciCredentialRequest, OpenId4VciCredentialSupported, + OpenId4VciIssuerMetadataDisplay, } from '../shared' import type { AgentContext, W3cCredential } from '@aries-framework/core' import type { SdJwtVcSignOptions } from '@aries-framework/sd-jwt-vc' -import type { CredentialOfferPayloadV1_0_11, CredentialSupported, MetadataDisplay } from '@sphereon/oid4vci-common' +import type { CredentialOfferPayloadV1_0_11 } from '@sphereon/oid4vci-common' export type PreAuthorizedCodeFlowConfig = { preAuthorizedCode?: string @@ -25,8 +26,8 @@ export type IssuerMetadata = { tokenEndpoint: string authorizationServer?: string - issuerDisplay?: MetadataDisplay[] - credentialsSupported: CredentialSupported[] + issuerDisplay?: OpenId4VciIssuerMetadataDisplay[] + credentialsSupported: OpenId4VciCredentialSupported[] } export type CreateIssuerOptions = Pick diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts index 473f2543c8..9e01ad5783 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts @@ -26,7 +26,7 @@ export class OpenId4VcVerifierModule implements Module { // Warn about experimental module const logger = dependencyManager.resolve(AgentConfig).logger logger.warn( - "The '@aries-framework/openid4vc-verifier' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages. Multi-Tenancy is not supported. " + "The '@aries-framework/openid4vc' Verifier module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages." ) // Register config diff --git a/packages/openid4vc/src/shared/models/index.ts b/packages/openid4vc/src/shared/models/index.ts index 2c8abfdd02..ae0fa4a1ee 100644 --- a/packages/openid4vc/src/shared/models/index.ts +++ b/packages/openid4vc/src/shared/models/index.ts @@ -4,11 +4,13 @@ import type { CredentialRequestJwtVcJsonLdAndLdpVc, CredentialRequestSdJwtVc, CredentialSupported, + MetadataDisplay, UniformCredentialRequest, } from '@sphereon/oid4vci-common' export type OpenId4VciCredentialSupportedWithId = CredentialSupported & { id: string } export type OpenId4VciCredentialSupported = CredentialSupported +export type OpenId4VciIssuerMetadataDisplay = MetadataDisplay export type OpenId4VciCredentialRequest = UniformCredentialRequest export type OpenId4VciCredentialRequestJwtVcJson = CredentialRequestJwtVcJson export type OpenId4VciCredentialRequestJwtVcJsonLdAndLdpVc = CredentialRequestJwtVcJsonLdAndLdpVc diff --git a/packages/openid4vc/src/shared/utils.ts b/packages/openid4vc/src/shared/utils.ts index c23d030178..40e698105c 100644 --- a/packages/openid4vc/src/shared/utils.ts +++ b/packages/openid4vc/src/shared/utils.ts @@ -1,4 +1,4 @@ -import type { AgentContext, VerificationMethod, JwaSignatureAlgorithm } from '@aries-framework/core' +import type { AgentContext, VerificationMethod, JwaSignatureAlgorithm, Key } from '@aries-framework/core' import type { DIDDocument, SigningAlgo } from '@sphereon/did-auth-siop' import { @@ -91,18 +91,13 @@ export async function generateRandomValues(agentContext: AgentContext, count: nu return await Promise.all(randomValuesPromises) } -export const getProofTypeFromVerificationMethod = ( - agentContext: AgentContext, - verificationMethod: VerificationMethod -) => { +export const getProofTypeFromKey = (agentContext: AgentContext, key: Key) => { const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) - const supportedSignatureSuite = signatureSuiteRegistry.getByVerificationMethodType(verificationMethod.type) - if (!supportedSignatureSuite) { - throw new AriesFrameworkError( - `Couldn't find a supported signature suite for the given verification method type '${verificationMethod.type}'.` - ) + const supportedSignatureSuites = signatureSuiteRegistry.getByKeyType(key.keyType) + if (supportedSignatureSuites.length === 0) { + throw new AriesFrameworkError(`Couldn't find a supported signature suite for the given key type '${key.keyType}'.`) } - return supportedSignatureSuite.proofType + return supportedSignatureSuites[0].proofType } diff --git a/packages/openid4vc/tests/utilsVp.ts b/packages/openid4vc/tests/utilsVp.ts index 8e741272c9..6d654aa026 100644 --- a/packages/openid4vc/tests/utilsVp.ts +++ b/packages/openid4vc/tests/utilsVp.ts @@ -3,6 +3,7 @@ import type { AgentContext, VerificationMethod } from '@aries-framework/core' import type { PresentationDefinitionV2 } from '@sphereon/pex-models' import { + getKeyFromVerificationMethod, W3cCredential, W3cIssuer, W3cCredentialSubject, @@ -14,7 +15,7 @@ import { SigningAlgo } from '@sphereon/did-auth-siop' import { staticOpOpenIdConfig } from '../src' import { staticOpSiopConfig } from '../src/openid4vc-verifier/OpenId4VcVerifierServiceOptions' -import { getProofTypeFromVerificationMethod } from '../src/shared/utils' +import { getProofTypeFromKey } from '../src/shared/utils' export const waltPortalOpenBadgeJwt = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa3RpUVFFcW0yeWFwWEJEdDFXRVZCM2RxZ3Z5emk5NkZ1RkFOWW1yZ1RyS1Y5I3o2TWt0aVFRRXFtMnlhcFhCRHQxV0VWQjNkcWd2eXppOTZGdUZBTlltcmdUcktWOSJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiT3BlbkJhZGdlQ3JlZGVudGlhbCJdLCJjcmVkZW50aWFsU3ViamVjdCI6e319LCJpc3MiOiJkaWQ6a2V5Ono2TWt0aVFRRXFtMnlhcFhCRHQxV0VWQjNkcWd2eXppOTZGdUZBTlltcmdUcktWOSIsInN1YiI6ImRpZDprZXk6ejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjIiwibmJmIjoxNzAwNzQzMzM1fQ.OcKPyaWeVV-78BWr8N4h2Cyvjtc9jzknAqvTA77hTbKCNCEbhGboo-S6yXHLC-3NWYQ1vVcqZmdPlIOrHZ7MDw' @@ -42,7 +43,8 @@ export const getOpenBadgeCredentialLdpVc = async ( }) const w3cs = agentContext.dependencyManager.resolve(W3cCredentialService) - const proofType = getProofTypeFromVerificationMethod(agentContext, holderVerificationMethod) + const key = getKeyFromVerificationMethod(holderVerificationMethod) + const proofType = getProofTypeFromKey(agentContext, key) const signedLdpVc = await w3cs.signCredential(agentContext, { format: ClaimFormat.LdpVc, credential, diff --git a/packages/sd-jwt-vc/package.json b/packages/sd-jwt-vc/package.json index 96a92abcd6..e3e25ee6e0 100644 --- a/packages/sd-jwt-vc/package.json +++ b/packages/sd-jwt-vc/package.json @@ -28,7 +28,7 @@ "@aries-framework/core": "^0.4.2", "class-transformer": "0.5.1", "class-validator": "0.14.0", - "@sd-jwt/core": "0.1.2-alpha.0" + "@sd-jwt/core": "0.1.2-alpha.6" }, "devDependencies": { "@aries-framework/node": "^0.4.2", diff --git a/packages/sd-jwt-vc/src/SdJwtVcService.ts b/packages/sd-jwt-vc/src/SdJwtVcService.ts index 9cf365fe1d..803e1366eb 100644 --- a/packages/sd-jwt-vc/src/SdJwtVcService.ts +++ b/packages/sd-jwt-vc/src/SdJwtVcService.ts @@ -240,7 +240,10 @@ export class SdJwtVcService { const didResolver = agentContext.dependencyManager.resolve(DidResolverService) const didDocument = await didResolver.resolveDidDocument(agentContext, didUrl) - return { verificationMethod: didDocument.dereferenceKey(didUrl), didDocument } + return { + verificationMethod: didDocument.dereferenceKey(didUrl, ['assertionMethod']), + didDocument, + } } private get hasher(): HasherAndAlgorithm { diff --git a/packages/sd-jwt-vc/src/__tests__/sdjwtvc.fixtures.ts b/packages/sd-jwt-vc/src/__tests__/sdjwtvc.fixtures.ts index 6d3d76e483..1cbb1b112c 100644 --- a/packages/sd-jwt-vc/src/__tests__/sdjwtvc.fixtures.ts +++ b/packages/sd-jwt-vc/src/__tests__/sdjwtvc.fixtures.ts @@ -1,5 +1,5 @@ export const simpleJwtVc = - 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzJ9.hcNF6_PnQO4Gm0vqD_iblyBknUG0PeQLbIpPJ5s0P4UCQ7YdSSNCNL7VNOfzzAxZRWbH5knhje0_xYl6OXQ-CA' + 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzJ9.hcNF6_PnQO4Gm0vqD_iblyBknUG0PeQLbIpPJ5s0P4UCQ7YdSSNCNL7VNOfzzAxZRWbH5knhje0_xYl6OXQ-CA~' export const simpleJwtVcPresentation = 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6IiN6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlcifQ.eyJjbGFpbSI6InNvbWUtY2xhaW0iLCJ2Y3QiOiJJZGVudGl0eUNyZWRlbnRpYWwiLCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoib0VOVnN4T1VpSDU0WDh3SkxhVmtpY0NSazAwd0JJUTRzUmdiazU0TjhNbyJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1rdHF0WE5HOENEVVk5UHJydG9TdEZ6ZUNuaHBNbWd4WUwxZ2lrY1czQnp2TlciLCJpYXQiOjE2OTgxNTE1MzJ9.hcNF6_PnQO4Gm0vqD_iblyBknUG0PeQLbIpPJ5s0P4UCQ7YdSSNCNL7VNOfzzAxZRWbH5knhje0_xYl6OXQ-CA~eyJhbGciOiJFZERTQSIsInR5cCI6ImtiK2p3dCJ9.eyJpYXQiOjE2OTgxNTE1MzIsIm5vbmNlIjoic2FsdCIsImF1ZCI6ImRpZDprZXk6elVDNzRWRXFxaEVIUWNndjR6YWdTUGtxRkp4dU5XdW9CUEtqSnVIRVRFVWVITG9TcVd0OTJ2aVNzbWFXank4MnkiLCJfc2RfaGFzaCI6IkN4SnFuQ1Btd0d6bjg4YTlDdGhta2pHZXFXbnlKVTVKc2NLMXJ1VThOS28ifQ.0QaDyJrvZO91o7gdKPduKQIj5Z1gBAdWPNE8-PFqhj_rC56_I5aL8QtlwL8Mdl6iSjpUPDQ4LAN2JgB2nNOFBw' diff --git a/yarn.lock b/yarn.lock index a5205bbf29..61eed51309 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2518,11 +2518,34 @@ resolved "https://registry.yarnpkg.com/@react-native/polyfills/-/polyfills-2.0.0.tgz#4c40b74655c83982c8cf47530ee7dc13d957b6aa" integrity sha512-K0aGNn1TjalKj+65D7ycc1//H9roAQ51GJVk5ZJQFb2teECGmzd86bYDC0aYdbRf7gtovescq4Zt6FR0tgXiHQ== -"@sd-jwt/core@0.1.2-alpha.0": - version "0.1.2-alpha.0" - resolved "https://registry.yarnpkg.com/@sd-jwt/core/-/core-0.1.2-alpha.0.tgz#a1b6ed2c7efc6d71d8fcd063b6624cf77c1eb21f" - integrity sha512-x4MVXar6WmPauZDRJ3aHwaY8o/bHzN77Ts7o43JKuuqIBFjPgAcSlRtd/Xk1rWhazFai4MCIwJDSQ1OQRJtNug== - dependencies: +"@sd-jwt/core@0.1.2-alpha.0", "@sd-jwt/core@0.1.2-alpha.6", "@sd-jwt/core@^0.1.2-alpha.2": + version "0.1.2-alpha.6" + resolved "https://registry.yarnpkg.com/@sd-jwt/core/-/core-0.1.2-alpha.6.tgz#f49f61e8efb19c4a1e141d2dd9044c09b0ae251e" + integrity sha512-BCGMEolOUCT0f18o+P30E6EUXZGjNB51zu+0JNswX6Y991XcxSqfeGh42W6IVtYp4J476BjxfAcc8yw00lkxKw== + dependencies: + "@sd-jwt/decode" "0.1.2-alpha.6" + "@sd-jwt/types" "0.1.2-alpha.6" + "@sd-jwt/utils" "0.1.2-alpha.6" + +"@sd-jwt/decode@0.1.2-alpha.6": + version "0.1.2-alpha.6" + resolved "https://registry.yarnpkg.com/@sd-jwt/decode/-/decode-0.1.2-alpha.6.tgz#a52e328d28d652711ec80cd296c9fe801ed90dc2" + integrity sha512-3G2VbatGLEh4fYCDyr3MCp6UjyyrbTgb3AGr3E1AVrK7dth+tJUdEawGN7jPAKDSCK2cWEJ5q+7OeKzE3I4RRA== + dependencies: + "@sd-jwt/types" "0.1.2-alpha.6" + "@sd-jwt/utils" "0.1.2-alpha.6" + +"@sd-jwt/types@0.1.2-alpha.6": + version "0.1.2-alpha.6" + resolved "https://registry.yarnpkg.com/@sd-jwt/types/-/types-0.1.2-alpha.6.tgz#6edaff78e28d45734b9b51032b39907c86db1e02" + integrity sha512-zRdVbdElbc0Et0PKwNUlNOO4k6XymH69ME+dvKy4PtXVJq6uy0hvKoX+jCoQuGzQphUEzKVf+fK4BaypR3dLjg== + +"@sd-jwt/utils@0.1.2-alpha.6": + version "0.1.2-alpha.6" + resolved "https://registry.yarnpkg.com/@sd-jwt/utils/-/utils-0.1.2-alpha.6.tgz#ef614f5a403fd413ba007afbfff87e9504e3b765" + integrity sha512-QuLEb543oPyaxQSVD46JVrabJFXdvvLcjlAYE+IN6KS9coc5GIgRWDZfNrjo5oh8Ez4rPHK4RdAnlZNjfgfgtA== + dependencies: + "@sd-jwt/types" "0.1.2-alpha.6" buffer "*" "@sideway/address@^4.1.3": @@ -2576,6 +2599,36 @@ resolved "https://registry.yarnpkg.com/@sovpro/delimited-stream/-/delimited-stream-1.1.0.tgz#4334bba7ee241036e580fdd99c019377630d26b4" integrity sha512-kQpk267uxB19X3X2T1mvNMjyvIEonpNSHrMlK5ZaBU6aZxw7wPbpgKJOjHN3+/GPVpXgAV9soVT2oyHpLkLtyw== +"@sphereon/did-auth-siop@0.6.0-unstable.0": + version "0.6.0-unstable.0" + resolved "https://registry.yarnpkg.com/@sphereon/did-auth-siop/-/did-auth-siop-0.6.0-unstable.0.tgz#1e7c8bafec4e36ec12eec7adfe48d21d0009af4a" + integrity sha512-k5z1sGOv+B8oz0H5zWHZE+1myDpM4RAB7UcS7Z3kNMgP27mK4VpvUFIYKJ3CW/3+QzxgDA5V7d/S6dqsjgmoKQ== + dependencies: + "@astronautlabs/jsonpath" "^1.1.2" + "@sphereon/did-uni-client" "^0.6.1" + "@sphereon/pex" "^3.0.0" + "@sphereon/pex-models" "^2.1.5" + "@sphereon/ssi-types" "^0.18.0" + "@sphereon/wellknown-dids-client" "^0.1.3" + cross-fetch "^4.0.0" + did-jwt "6.11.6" + did-resolver "^4.1.0" + events "^3.3.0" + language-tags "^1.0.9" + multiformats "^11.0.2" + qs "^6.11.2" + sha.js "^2.4.11" + uint8arrays "^3.1.1" + uuid "^9.0.0" + +"@sphereon/did-uni-client@^0.6.1": + version "0.6.1" + resolved "https://registry.yarnpkg.com/@sphereon/did-uni-client/-/did-uni-client-0.6.1.tgz#5fe7fa2b87c22f939c95d388b6fcf9e6e93c70a8" + integrity sha512-ryIPq9fAp8UuaN0ZQ16Gong5n5SX8G+SjNQ3x3Uy/pmd6syxh97kkmrfbna7a8dTmbP8YdNtgPLpcNbhLPMClQ== + dependencies: + cross-fetch "^4.0.0" + did-resolver "^4.1.0" + "@sphereon/oid4vci-client@0.8.2-next.26": version "0.8.2-next.26" resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-client/-/oid4vci-client-0.8.2-next.26.tgz#aa36af4fa81fb872d4292d21304e3c6aa9f6209d" @@ -2609,6 +2662,11 @@ resolved "https://registry.yarnpkg.com/@sphereon/pex-models/-/pex-models-2.1.2.tgz#e1a0ce16ccc6b32128fc8c2da79d65fc35f6d10f" integrity sha512-Ec1qZl8tuPd+s6E+ZM7v+HkGkSOjGDMLNN1kqaxAfWpITBYtTLb+d5YvwjvBZ1P2upZ7zwNER97FfW5n/30y2w== +"@sphereon/pex-models@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@sphereon/pex-models/-/pex-models-2.1.5.tgz#ba4474a3783081392b72403c4c8ee6da3d2e5585" + integrity sha512-7THexvdYUK/Dh8olBB46ErT9q/RnecnMdb5r2iwZ6be0Dt4vQLAUN7QU80H0HZBok4jRTb8ydt12x0raBSTHOg== + "@sphereon/pex@^2.2.2": version "2.2.2" resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-2.2.2.tgz#3df9ed75281b46f0899256774060ed2ff982fade" @@ -2623,14 +2681,38 @@ nanoid "^3.3.6" string.prototype.matchall "^4.0.8" -"@sphereon/ssi-types@0.17.6-unstable.69", "@sphereon/ssi-types@^0.17.5", "@sphereon/ssi-types@^0.17.6-unstable.69": - version "0.17.6-unstable.74" - resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.17.6-unstable.74.tgz#bbca58cdc1d89f0176b145b0c52a06bd1eeeec9a" - integrity sha512-8B1XzLYc9QWv7AXeOiE1d5y30XkFV9/wr78t3B9CEtNoB0QANP9fPQ+WWcMdAezSgEWXD6nq+11XF8ZtTYB4Ww== +"@sphereon/pex@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-3.0.0.tgz#97ac049b279a50115f02c85185d1f348af9d6b91" + integrity sha512-viD3Enwt/Wf/RoTBbZXttNAcE0Scyb+UufwkNaLU4sD2nNkvi3VuJ4dSMMFGm3QlIgBWxSm1N4DnSm+LRMZidQ== + dependencies: + "@astronautlabs/jsonpath" "^1.1.2" + "@sd-jwt/core" "^0.1.2-alpha.2" + "@sphereon/pex-models" "^2.1.5" + "@sphereon/ssi-types" "0.18.0" + ajv "^8.12.0" + ajv-formats "^2.1.1" + jwt-decode "^3.1.2" + nanoid "^3.3.7" + string.prototype.matchall "^4.0.10" + +"@sphereon/ssi-types@0.17.6-unstable.69", "@sphereon/ssi-types@0.18.0", "@sphereon/ssi-types@^0.17.5", "@sphereon/ssi-types@^0.18.0", "@sphereon/ssi-types@^0.9.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.18.0.tgz#b4f3a4a9e4e5b28719ce729b679d71d9d220cc26" + integrity sha512-D2n42NAhHCwpL4K7BqQXO9dYQ8n3st/1eJQrLqokJ18B9r2gury3km4cp+ZdiIxfefUaP9RBCeuWaiRUvjZ94w== dependencies: "@sd-jwt/core" "0.1.2-alpha.0" jwt-decode "^3.1.2" +"@sphereon/wellknown-dids-client@^0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@sphereon/wellknown-dids-client/-/wellknown-dids-client-0.1.3.tgz#4711599ed732903e9f45fe051660f925c9b508a4" + integrity sha512-TAT24L3RoXD8ocrkTcsz7HuJmgjNjdoV6IXP1p3DdaI/GqkynytXE3J1+F7vUFMRYwY5nW2RaXSgDQhrFJemaA== + dependencies: + "@sphereon/ssi-types" "^0.9.0" + cross-fetch "^3.1.5" + jwt-decode "^3.1.2" + "@stablelib/aead@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@stablelib/aead/-/aead-1.0.1.tgz#c4b1106df9c23d1b867eb9b276d8f42d5fc4c0c3" @@ -4904,7 +4986,7 @@ credentials-context@^2.0.0: resolved "https://registry.yarnpkg.com/credentials-context/-/credentials-context-2.0.0.tgz#68a9a1a88850c398d3bba4976c8490530af093e8" integrity sha512-/mFKax6FK26KjgV2KW2D4YqKgoJ5DVJpNt87X2Jc9IxT2HBMy7nEIlc+n7pEi+YFFe721XqrvZPd+jbyyBjsvQ== -cross-fetch@^3.1.8: +cross-fetch@^3.1.5, cross-fetch@^3.1.8: version "3.1.8" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== @@ -5169,7 +5251,7 @@ detect-newline@^3.0.0: resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== -did-jwt@^6.11.6: +did-jwt@6.11.6, did-jwt@^6.11.6: version "6.11.6" resolved "https://registry.yarnpkg.com/did-jwt/-/did-jwt-6.11.6.tgz#3eeb30d6bd01f33bfa17089574915845802a7d44" integrity sha512-OfbWknRxJuUqH6Lk0x+H1FsuelGugLbBDEwsoJnicFOntIG/A4y19fn0a8RLxaQbWQ5gXg0yDq5E2huSBiiXzw== @@ -6993,7 +7075,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -8278,6 +8360,18 @@ ky@^0.25.1: resolved "https://registry.yarnpkg.com/ky/-/ky-0.25.1.tgz#0df0bd872a9cc57e31acd5dbc1443547c881bfbc" integrity sha512-PjpCEWlIU7VpiMVrTwssahkYXX1by6NCT0fhTUX34F3DTinARlgMpriuroolugFPcMgpPWrOW4mTb984Qm1RXA== +language-subtag-registry@^0.3.20: + version "0.3.22" + resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d" + integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w== + +language-tags@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.9.tgz#1ffdcd0ec0fafb4b1be7f8b11f306ad0f9c08777" + integrity sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA== + dependencies: + language-subtag-registry "^0.3.20" + lerna@^6.5.1: version "6.6.1" resolved "https://registry.yarnpkg.com/lerna/-/lerna-6.6.1.tgz#4897171aed64e244a2d0f9000eef5c5b228f9332" @@ -9338,6 +9432,11 @@ multer@^1.4.5-lts.1: type-is "^1.6.4" xtend "^4.0.0" +multiformats@^11.0.2: + version "11.0.2" + resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-11.0.2.tgz#b14735efc42cd8581e73895e66bebb9752151b60" + integrity sha512-b5mYMkOkARIuVZCpvijFj9a6m5wMVLC7cf/jIPd5D/ARDOfLC5+IFkbgDXQgcU2goIsTD/O9NY4DI/Mt4OGvlg== + multiformats@^9.4.2, multiformats@^9.6.5, multiformats@^9.9.0: version "9.9.0" resolved "https://registry.yarnpkg.com/multiformats/-/multiformats-9.9.0.tgz#c68354e7d21037a8f1f8833c8ccd68618e8f1d37" @@ -9364,7 +9463,7 @@ nan@^2.11.1: resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== -nanoid@^3.3.6: +nanoid@^3.3.6, nanoid@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== @@ -10706,6 +10805,13 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" +qs@^6.11.2: + version "6.11.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9" + integrity sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA== + dependencies: + side-channel "^1.0.4" + query-string@^7.0.1: version "7.1.3" resolved "https://registry.yarnpkg.com/query-string/-/query-string-7.1.3.tgz#a1cf90e994abb113a325804a972d98276fe02328" @@ -11437,6 +11543,14 @@ setprototypeof@1.2.0: resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== +sha.js@^2.4.11: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + shallow-clone@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" @@ -11798,7 +11912,7 @@ string-width@^1.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string.prototype.matchall@^4.0.8: +string.prototype.matchall@^4.0.10, string.prototype.matchall@^4.0.8: version "4.0.10" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz#a1553eb532221d4180c51581d6072cd65d1ee100" integrity sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ== @@ -12487,7 +12601,7 @@ uglify-js@^3.1.4: resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.17.4.tgz#61678cf5fa3f5b7eb789bb345df29afb8257c22c" integrity sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g== -uint8arrays@^3.0.0: +uint8arrays@^3.0.0, uint8arrays@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.1.1.tgz#2d8762acce159ccd9936057572dade9459f65ae0" integrity sha512-+QJa8QRnbdXVpHYjLoTpJIdCTiw9Ir62nocClWuXIq2JIh4Uta0cQsTSpFL678p2CN8B+XSApwcU+pQEqVpKWg== From 8afe0fdae139e14d6fb19528c43579d8e5a1759b Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sat, 20 Jan 2024 19:47:04 +0700 Subject: [PATCH 098/115] dependency updates Signed-off-by: Timo Glastra --- package.json | 4 +- packages/core/package.json | 9 +- packages/openid4vc/package.json | 15 +-- packages/sd-jwt-vc/package.json | 2 +- yarn.lock | 232 +++++++++++++++++++------------- 5 files changed, 146 insertions(+), 116 deletions(-) diff --git a/package.json b/package.json index 579c63816f..d773b4143c 100644 --- a/package.json +++ b/package.json @@ -62,9 +62,7 @@ "ws": "^8.13.0" }, "resolutions": { - "@types/node": "18.18.8", - "@sphereon/ssi-types": "^0.18.0", - "@sd-jwt/core": "0.1.2-alpha.6" + "@types/node": "18.18.8" }, "engines": { "node": ">=18" diff --git a/packages/core/package.json b/packages/core/package.json index 88a7d93fb1..e06adb7e09 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -27,14 +27,11 @@ "@digitalcredentials/jsonld-signatures": "^9.3.1", "@digitalcredentials/vc": "^1.1.2", "@multiformats/base-x": "^4.0.1", - "@sphereon/pex": "^2.2.2", - "@sphereon/pex-models": "^2.1.2", - "@sphereon/ssi-types": "^0.17.5", + "@sphereon/pex": "^3.0.1", + "@sphereon/pex-models": "^2.1.5", + "@sphereon/ssi-types": "^0.18.1", "@stablelib/ed25519": "^1.0.2", "@stablelib/sha256": "^1.0.1", - "@sphereon/pex": "^2.2.2", - "@sphereon/pex-models": "^2.1.2", - "@sphereon/ssi-types": "^0.17.5", "@types/ws": "^8.5.4", "abort-controller": "^3.0.0", "big-integer": "^1.6.51", diff --git a/packages/openid4vc/package.json b/packages/openid4vc/package.json index 3f2a00c8e6..e3d4d8a6ad 100644 --- a/packages/openid4vc/package.json +++ b/packages/openid4vc/package.json @@ -26,15 +26,11 @@ "dependencies": { "@aries-framework/askar": "^0.4.2", "@aries-framework/core": "0.4.2", - "@sphereon/ssi-types": "^0.18.0", - "@sphereon/oid4vci-client": "0.8.2-next.26", - "@sphereon/oid4vci-common": "0.8.2-next.26", - "@sphereon/oid4vci-issuer": "0.8.2-next.26", - "@sphereon/did-auth-siop": "0.6.0-unstable.0", - "@sphereon/pex": "^3.0.0", - "@sphereon/pex-models": "^2.1.2", - "body-parser": "^1.20.2", - "jsonpath": "^1.1.1" + "@sphereon/ssi-types": "^0.18.1", + "@sphereon/oid4vci-client": "0.8.2-next.34", + "@sphereon/oid4vci-common": "0.8.2-next.34", + "@sphereon/oid4vci-issuer": "0.8.2-next.34", + "@sphereon/did-auth-siop": "0.6.0-unstable.0" }, "devDependencies": { "@aries-framework/node": "^0.4.2", @@ -42,7 +38,6 @@ "@aries-framework/tenants": "^0.4.2", "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", "@types/express": "^4.17.21", - "@types/jsonpath": "^0.2.4", "express": "^4.18.2", "nock": "^13.3.0", "rimraf": "^4.4.0", diff --git a/packages/sd-jwt-vc/package.json b/packages/sd-jwt-vc/package.json index e3e25ee6e0..8f0db38ec3 100644 --- a/packages/sd-jwt-vc/package.json +++ b/packages/sd-jwt-vc/package.json @@ -28,7 +28,7 @@ "@aries-framework/core": "^0.4.2", "class-transformer": "0.5.1", "class-validator": "0.14.0", - "@sd-jwt/core": "0.1.2-alpha.6" + "@sd-jwt/core": "^0.2.0" }, "devDependencies": { "@aries-framework/node": "^0.4.2", diff --git a/yarn.lock b/yarn.lock index 61eed51309..89102788a1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2518,34 +2518,89 @@ resolved "https://registry.yarnpkg.com/@react-native/polyfills/-/polyfills-2.0.0.tgz#4c40b74655c83982c8cf47530ee7dc13d957b6aa" integrity sha512-K0aGNn1TjalKj+65D7ycc1//H9roAQ51GJVk5ZJQFb2teECGmzd86bYDC0aYdbRf7gtovescq4Zt6FR0tgXiHQ== -"@sd-jwt/core@0.1.2-alpha.0", "@sd-jwt/core@0.1.2-alpha.6", "@sd-jwt/core@^0.1.2-alpha.2": - version "0.1.2-alpha.6" - resolved "https://registry.yarnpkg.com/@sd-jwt/core/-/core-0.1.2-alpha.6.tgz#f49f61e8efb19c4a1e141d2dd9044c09b0ae251e" - integrity sha512-BCGMEolOUCT0f18o+P30E6EUXZGjNB51zu+0JNswX6Y991XcxSqfeGh42W6IVtYp4J476BjxfAcc8yw00lkxKw== - dependencies: - "@sd-jwt/decode" "0.1.2-alpha.6" - "@sd-jwt/types" "0.1.2-alpha.6" - "@sd-jwt/utils" "0.1.2-alpha.6" - -"@sd-jwt/decode@0.1.2-alpha.6": - version "0.1.2-alpha.6" - resolved "https://registry.yarnpkg.com/@sd-jwt/decode/-/decode-0.1.2-alpha.6.tgz#a52e328d28d652711ec80cd296c9fe801ed90dc2" - integrity sha512-3G2VbatGLEh4fYCDyr3MCp6UjyyrbTgb3AGr3E1AVrK7dth+tJUdEawGN7jPAKDSCK2cWEJ5q+7OeKzE3I4RRA== - dependencies: - "@sd-jwt/types" "0.1.2-alpha.6" - "@sd-jwt/utils" "0.1.2-alpha.6" - -"@sd-jwt/types@0.1.2-alpha.6": - version "0.1.2-alpha.6" - resolved "https://registry.yarnpkg.com/@sd-jwt/types/-/types-0.1.2-alpha.6.tgz#6edaff78e28d45734b9b51032b39907c86db1e02" - integrity sha512-zRdVbdElbc0Et0PKwNUlNOO4k6XymH69ME+dvKy4PtXVJq6uy0hvKoX+jCoQuGzQphUEzKVf+fK4BaypR3dLjg== - -"@sd-jwt/utils@0.1.2-alpha.6": - version "0.1.2-alpha.6" - resolved "https://registry.yarnpkg.com/@sd-jwt/utils/-/utils-0.1.2-alpha.6.tgz#ef614f5a403fd413ba007afbfff87e9504e3b765" - integrity sha512-QuLEb543oPyaxQSVD46JVrabJFXdvvLcjlAYE+IN6KS9coc5GIgRWDZfNrjo5oh8Ez4rPHK4RdAnlZNjfgfgtA== - dependencies: - "@sd-jwt/types" "0.1.2-alpha.6" +"@sd-jwt/core@0.1.2-alpha.0": + version "0.1.2-alpha.0" + resolved "https://registry.yarnpkg.com/@sd-jwt/core/-/core-0.1.2-alpha.0.tgz#a1b6ed2c7efc6d71d8fcd063b6624cf77c1eb21f" + integrity sha512-x4MVXar6WmPauZDRJ3aHwaY8o/bHzN77Ts7o43JKuuqIBFjPgAcSlRtd/Xk1rWhazFai4MCIwJDSQ1OQRJtNug== + dependencies: + buffer "*" + +"@sd-jwt/core@^0.1.2-alpha.2": + version "0.1.2-alpha.9" + resolved "https://registry.yarnpkg.com/@sd-jwt/core/-/core-0.1.2-alpha.9.tgz#ddd1c74db273e43fb41a43a25e33367e3d5afc30" + integrity sha512-yMabWCD1ImxFmgaqMg95TOWCiJpjXrVFYWBtCeHUk+O0SuGScB30KVcDmEo6/8vm5CyAAJg2TT156MJoDkqTDA== + dependencies: + "@sd-jwt/decode" "0.1.2-alpha.9" + "@sd-jwt/present" "0.1.2-alpha.9" + "@sd-jwt/types" "0.1.2-alpha.9" + "@sd-jwt/utils" "0.1.2-alpha.9" + +"@sd-jwt/core@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@sd-jwt/core/-/core-0.2.0.tgz#e06736ff4920570660fce4e040fe40e900c7fcfa" + integrity sha512-KxsJm/NAvKkbqOXaIq7Pndn70++bm8QNzzBh1KOwhlRub7LVrqeEkie/wrI/sAH+S+5exG0HTbY95F86nHiq7Q== + dependencies: + "@sd-jwt/decode" "0.2.0" + "@sd-jwt/present" "0.2.0" + "@sd-jwt/types" "0.2.0" + "@sd-jwt/utils" "0.2.0" + +"@sd-jwt/decode@0.1.2-alpha.9": + version "0.1.2-alpha.9" + resolved "https://registry.yarnpkg.com/@sd-jwt/decode/-/decode-0.1.2-alpha.9.tgz#02bb88725ba8e3ca0957624ef3eee7d2e3dc2ef9" + integrity sha512-3Hx5yd1b9gDC0wK7ZkNVzKevyvdGGkmV+mK7/LBUIR+q5SLZlwOmIHz80EM+8Eg0WFAnAmRgWKjn7jWWRQO5dw== + dependencies: + "@sd-jwt/types" "0.1.2-alpha.9" + "@sd-jwt/utils" "0.1.2-alpha.9" + +"@sd-jwt/decode@0.2.0", "@sd-jwt/decode@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@sd-jwt/decode/-/decode-0.2.0.tgz#44211418fd0884a160f8223feedfe04ae52398c4" + integrity sha512-nmiZN3SQ4ApapEu+rS1h/YAkDIq3exgN7swSCsEkrxSEwnBSbXtISIY/sv+EmwnehF1rcKbivHfHNxOWYtlxvg== + dependencies: + "@sd-jwt/types" "0.2.0" + "@sd-jwt/utils" "0.2.0" + +"@sd-jwt/present@0.1.2-alpha.9": + version "0.1.2-alpha.9" + resolved "https://registry.yarnpkg.com/@sd-jwt/present/-/present-0.1.2-alpha.9.tgz#f0577dcc66dc08e6bc91faf108565e0fc40d2383" + integrity sha512-LR7uIoC4As2EmGke+lCv2GifG2Xmr4iEFacx30GJW1n35T7vRBDpldIuoMNqHmsR+z3eHx/TWtjrXsh7lGcFtw== + dependencies: + "@sd-jwt/types" "0.1.2-alpha.9" + "@sd-jwt/utils" "0.1.2-alpha.9" + +"@sd-jwt/present@0.2.0", "@sd-jwt/present@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@sd-jwt/present/-/present-0.2.0.tgz#01ecbd09dd21287be892b36d754a79c8629387f2" + integrity sha512-6xDBiB+UqCwW8k7O7OUJ7BgC/8zcO+AD5ZX1k4I6yjDM9vscgPulSVxT/yUH+Aov3cZ/BKvfKC0qDEZkHmP/kg== + dependencies: + "@sd-jwt/types" "0.2.0" + "@sd-jwt/utils" "0.2.0" + +"@sd-jwt/types@0.1.2-alpha.9": + version "0.1.2-alpha.9" + resolved "https://registry.yarnpkg.com/@sd-jwt/types/-/types-0.1.2-alpha.9.tgz#198899e3a98f9329f35b20fd8af5c1ee37e1e739" + integrity sha512-j7Nf3RhQshkEjf3RQhF5hWMMOPQmzwhXBUhjcOoF5eJNgkpF6R13ryb3GDJHkRomIhkygaWaFEzC+ioRAZ7FzQ== + +"@sd-jwt/types@0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@sd-jwt/types/-/types-0.2.0.tgz#3cb50392e1b76ce69453f403c71c937a6e202352" + integrity sha512-16WFRcL/maG0/JxN9UCSx07/vJ2SDbGscv9gDLmFLgJzhJcGPer41XfI6aDfVARYP430wHFixChfY/n7qC1L/Q== + +"@sd-jwt/utils@0.1.2-alpha.9": + version "0.1.2-alpha.9" + resolved "https://registry.yarnpkg.com/@sd-jwt/utils/-/utils-0.1.2-alpha.9.tgz#efbde280798afb964e829726214167f50e7022a3" + integrity sha512-oPNWO/XDUkJxdEyOZvmLoqCo0uwiu5Xk0wGkmpwB9KtzeaioVW3JziFUswEczE9RED4+dOWtQwbSpEcy1DEWQw== + dependencies: + "@sd-jwt/types" "0.1.2-alpha.9" + buffer "*" + +"@sd-jwt/utils@0.2.0", "@sd-jwt/utils@^0.2.0": + version "0.2.0" + resolved "https://registry.yarnpkg.com/@sd-jwt/utils/-/utils-0.2.0.tgz#ef52b744116e874f72ec01978f0631ad5a131eb7" + integrity sha512-oHCfRYVHCb5RNwdq3eHAt7P9d7TsEaSM1TTux+xl1I9PeQGLtZETnto9Gchtzn8FlTrMdVsLlcuAcK6Viwj1Qw== + dependencies: + "@sd-jwt/types" "0.2.0" buffer "*" "@sideway/address@^4.1.3": @@ -2629,58 +2684,39 @@ cross-fetch "^4.0.0" did-resolver "^4.1.0" -"@sphereon/oid4vci-client@0.8.2-next.26": - version "0.8.2-next.26" - resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-client/-/oid4vci-client-0.8.2-next.26.tgz#aa36af4fa81fb872d4292d21304e3c6aa9f6209d" - integrity sha512-shba0CYsMTcbTN8Lf4fvpFD/cMlILg1/2Zy6KoKw6WS/UKnSJ74B9E5n6uopwrh69UqfHDR8uIkum+YWjpxQOA== +"@sphereon/oid4vci-client@0.8.2-next.34": + version "0.8.2-next.34" + resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-client/-/oid4vci-client-0.8.2-next.34.tgz#6b24b669f71ca6575bacf5305aeb70a1f22a2e57" + integrity sha512-p/iTvXz9XckNDgP2AmtZPWmvpNUCAas1Pe9XQIZLL/TXYNtgO32P+woY6FAeNrHAAfvyVu/DCuosps31bB/7lA== dependencies: - "@sphereon/oid4vci-common" "0.8.2-next.26+7577e3d" - "@sphereon/ssi-types" "0.17.6-unstable.69" + "@sphereon/oid4vci-common" "0.8.2-next.34+b3f0cf1" + "@sphereon/ssi-types" "^0.18.0" cross-fetch "^3.1.8" debug "^4.3.4" -"@sphereon/oid4vci-common@0.8.2-next.26", "@sphereon/oid4vci-common@0.8.2-next.26+7577e3d": - version "0.8.2-next.26" - resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-common/-/oid4vci-common-0.8.2-next.26.tgz#b281666f566fec36c30e6776ff62ec46f63bd0c7" - integrity sha512-BPB7lj8YtGez3UV+nN0EGDJcJW/4j4y59TeR5vW1a4VQOhCiEKZ6ttqqKV9UUgv+IampnlXiYdyr6zSorDMRjA== +"@sphereon/oid4vci-common@0.8.2-next.34", "@sphereon/oid4vci-common@0.8.2-next.34+b3f0cf1": + version "0.8.2-next.34" + resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-common/-/oid4vci-common-0.8.2-next.34.tgz#821186b7153b4dadbeddd2d7ca0d65e5a8acac84" + integrity sha512-jkuusNR0aa8itQwYgQMo4Jzy0ViyUFvUK4BS9GiQmemD9fBbqAmvXNRXCHz2KI9H1wbhw45CF+BKVNYB1REdHA== dependencies: - "@sphereon/ssi-types" "0.17.6-unstable.69" + "@sphereon/ssi-types" "^0.18.0" cross-fetch "^3.1.8" jwt-decode "^3.1.2" -"@sphereon/oid4vci-issuer@0.8.2-next.26": - version "0.8.2-next.26" - resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-issuer/-/oid4vci-issuer-0.8.2-next.26.tgz#2ebbba253783628439816ad902064348d631c1b4" - integrity sha512-zOe/it83UDC4Xi1yuG19DAW755p7g7z37NwvE6sU8TW7ksosHYcZgNhDzdZoG0eBdqVe6bVN1plYxwyMrL3NHg== +"@sphereon/oid4vci-issuer@0.8.2-next.34": + version "0.8.2-next.34" + resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-issuer/-/oid4vci-issuer-0.8.2-next.34.tgz#b04cbb16b166d2081dcfd730f7159cc716af5179" + integrity sha512-+p+U9AwuYLIZvzuheo0p1iWeeFLSAL3U+vy9boF+uBRhhBwv05oJMzKCrPjPFPbNCaCGffUC/KYRJkl4QR/VsA== dependencies: - "@sphereon/oid4vci-common" "0.8.2-next.26+7577e3d" - "@sphereon/ssi-types" "0.17.6-unstable.69" + "@sphereon/oid4vci-common" "0.8.2-next.34+b3f0cf1" + "@sphereon/ssi-types" "^0.18.0" uuid "^9.0.0" -"@sphereon/pex-models@^2.1.2": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@sphereon/pex-models/-/pex-models-2.1.2.tgz#e1a0ce16ccc6b32128fc8c2da79d65fc35f6d10f" - integrity sha512-Ec1qZl8tuPd+s6E+ZM7v+HkGkSOjGDMLNN1kqaxAfWpITBYtTLb+d5YvwjvBZ1P2upZ7zwNER97FfW5n/30y2w== - "@sphereon/pex-models@^2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@sphereon/pex-models/-/pex-models-2.1.5.tgz#ba4474a3783081392b72403c4c8ee6da3d2e5585" integrity sha512-7THexvdYUK/Dh8olBB46ErT9q/RnecnMdb5r2iwZ6be0Dt4vQLAUN7QU80H0HZBok4jRTb8ydt12x0raBSTHOg== -"@sphereon/pex@^2.2.2": - version "2.2.2" - resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-2.2.2.tgz#3df9ed75281b46f0899256774060ed2ff982fade" - integrity sha512-NkR8iDTC2PSnYsOHlG2M2iOpFTTbzszs2/pL3iK3Dlv9QYLqX7NtPAlmeSwaoVP1NB1ewcs6U1DtemQAD+90yQ== - dependencies: - "@astronautlabs/jsonpath" "^1.1.2" - "@sphereon/pex-models" "^2.1.2" - "@sphereon/ssi-types" "^0.17.5" - ajv "^8.12.0" - ajv-formats "^2.1.1" - jwt-decode "^3.1.2" - nanoid "^3.3.6" - string.prototype.matchall "^4.0.8" - "@sphereon/pex@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-3.0.0.tgz#97ac049b279a50115f02c85185d1f348af9d6b91" @@ -2696,7 +2732,24 @@ nanoid "^3.3.7" string.prototype.matchall "^4.0.10" -"@sphereon/ssi-types@0.17.6-unstable.69", "@sphereon/ssi-types@0.18.0", "@sphereon/ssi-types@^0.17.5", "@sphereon/ssi-types@^0.18.0", "@sphereon/ssi-types@^0.9.0": +"@sphereon/pex@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-3.0.1.tgz#e7d9d36c7c921ab97190a735c67e0a2632432e3b" + integrity sha512-rj+GhFfV5JLyo7dTIA3htWlrT+f6tayF9JRAGxdsIYBfYictLi9BirQ++JRBXsiq7T5zMnfermz4RGi3cvt13Q== + dependencies: + "@astronautlabs/jsonpath" "^1.1.2" + "@sd-jwt/decode" "^0.2.0" + "@sd-jwt/present" "^0.2.0" + "@sd-jwt/utils" "^0.2.0" + "@sphereon/pex-models" "^2.1.5" + "@sphereon/ssi-types" "0.18.1" + ajv "^8.12.0" + ajv-formats "^2.1.1" + jwt-decode "^3.1.2" + nanoid "^3.3.7" + string.prototype.matchall "^4.0.10" + +"@sphereon/ssi-types@0.18.0", "@sphereon/ssi-types@^0.18.0": version "0.18.0" resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.18.0.tgz#b4f3a4a9e4e5b28719ce729b679d71d9d220cc26" integrity sha512-D2n42NAhHCwpL4K7BqQXO9dYQ8n3st/1eJQrLqokJ18B9r2gury3km4cp+ZdiIxfefUaP9RBCeuWaiRUvjZ94w== @@ -2704,6 +2757,21 @@ "@sd-jwt/core" "0.1.2-alpha.0" jwt-decode "^3.1.2" +"@sphereon/ssi-types@0.18.1", "@sphereon/ssi-types@^0.18.1": + version "0.18.1" + resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.18.1.tgz#c00e4939149f4e441fae56af860735886a4c33a5" + integrity sha512-uM0gb1woyc0R+p+qh8tVDi15ZWmpzo9BP0iBp/yRkJar7gAfgwox/yvtEToaH9jROKnDCwL3DDQCDeNucpMkwg== + dependencies: + "@sd-jwt/decode" "^0.2.0" + jwt-decode "^3.1.2" + +"@sphereon/ssi-types@^0.9.0": + version "0.9.0" + resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.9.0.tgz#d140eb6abd77381926d0da7ac51b3c4b96a31b4b" + integrity sha512-umCr/syNcmvMMbQ+i/r/mwjI1Qw2aFPp9AwBTvTo1ailAVaaJjJGPkkVz1K9/2NZATNdDiQ3A8yGzdVJoKh9pA== + dependencies: + jwt-decode "^3.1.2" + "@sphereon/wellknown-dids-client@^0.1.3": version "0.1.3" resolved "https://registry.yarnpkg.com/@sphereon/wellknown-dids-client/-/wellknown-dids-client-0.1.3.tgz#4711599ed732903e9f45fe051660f925c9b508a4" @@ -4067,24 +4135,6 @@ body-parser@1.20.1: type-is "~1.6.18" unpipe "1.0.0" -body-parser@^1.20.2: - version "1.20.2" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" - integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== - dependencies: - bytes "3.1.2" - content-type "~1.0.5" - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - http-errors "2.0.0" - iconv-lite "0.4.24" - on-finished "2.4.1" - qs "6.11.0" - raw-body "2.5.2" - type-is "~1.6.18" - unpipe "1.0.0" - borc@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/borc/-/borc-3.0.0.tgz#49ada1be84de86f57bb1bb89789f34c186dfa4fe" @@ -4785,7 +4835,7 @@ content-disposition@0.5.4: dependencies: safe-buffer "5.2.1" -content-type@~1.0.4, content-type@~1.0.5: +content-type@~1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== @@ -9463,7 +9513,7 @@ nan@^2.11.1: resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb" integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ== -nanoid@^3.3.6, nanoid@^3.3.7: +nanoid@^3.3.7: version "3.3.7" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== @@ -10847,16 +10897,6 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" -raw-body@2.5.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" - integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== - dependencies: - bytes "3.1.2" - http-errors "2.0.0" - iconv-lite "0.4.24" - unpipe "1.0.0" - rc@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" @@ -11912,7 +11952,7 @@ string-width@^1.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string.prototype.matchall@^4.0.10, string.prototype.matchall@^4.0.8: +string.prototype.matchall@^4.0.10: version "4.0.10" resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz#a1553eb532221d4180c51581d6072cd65d1ee100" integrity sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ== From 1f5729cae8f2ca3118222523324946bd521f21f5 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sat, 20 Jan 2024 19:47:11 +0700 Subject: [PATCH 099/115] export type Signed-off-by: Timo Glastra --- packages/core/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index af3a0077b6..1d685f561b 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -89,6 +89,8 @@ export { LinkedAttachment, LinkedAttachmentOptions } from './utils/LinkedAttachm import { parseInvitationUrl } from './utils/parseInvitation' import { uuid, isValidUuid } from './utils/uuid' +export type { Optional } from './utils/type' + const utils = { uuid, isValidUuid, From 171a86de2caf83ca1554c2d2cb9afa5137c633e0 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sat, 20 Jan 2024 19:51:47 +0700 Subject: [PATCH 100/115] some refactoring Signed-off-by: Timo Glastra --- .../openid4vc-issuer/OpenId4VcIssuerApi.ts | 24 ++---- .../openid4vc-issuer/OpenId4VcIssuerModule.ts | 13 ++-- .../OpenId4VcIssuerModuleConfig.ts | 14 ++-- .../OpenId4VcIssuerService.ts | 27 +++---- .../OpenId4VcIssuerServiceOptions.ts | 78 +++---------------- .../router/accessTokenEndpoint.ts | 69 ++++++++++++---- .../router/credentialEndpoint.ts | 28 +++++-- .../src/openid4vc-issuer/router/index.ts | 6 +- .../router/metadataEndpoint.ts | 4 +- .../openid4vc-issuer/router/requestContext.ts | 68 +--------------- .../shared/{router.ts => router/context.ts} | 11 ++- .../router/express.ts | 0 packages/openid4vc/src/shared/router/index.ts | 3 + .../openid4vc/src/shared/router/tenants.ts | 57 ++++++++++++++ packages/openid4vc/src/shared/utils.ts | 5 -- 15 files changed, 192 insertions(+), 215 deletions(-) rename packages/openid4vc/src/shared/{router.ts => router/context.ts} (67%) rename packages/openid4vc/src/{openid4vc-issuer => shared}/router/express.ts (100%) create mode 100644 packages/openid4vc/src/shared/router/index.ts create mode 100644 packages/openid4vc/src/shared/router/tenants.ts diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts index e4b4038e36..ad13a2fed4 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts @@ -1,6 +1,6 @@ import type { CreateCredentialResponseOptions, - CreateCredentialOfferOptions, + OpenId4VciCreateCredentialOfferOptions, CredentialOffer, } from './OpenId4VcIssuerServiceOptions' import type { OpenId4VcIssuerRecordProps } from './repository/OpenId4VcIssuerRecord' @@ -19,23 +19,11 @@ import { OpenId4VcIssuerService } from './OpenId4VcIssuerService' */ @injectable() export class OpenId4VcIssuerApi { - /** - * Configuration for the credentials module - */ - public readonly config: OpenId4VcIssuerModuleConfig - - private agentContext: AgentContext - private openId4VcIssuerService: OpenId4VcIssuerService - public constructor( - agentContext: AgentContext, - openId4VcIssuerService: OpenId4VcIssuerService, - config: OpenId4VcIssuerModuleConfig - ) { - this.agentContext = agentContext - this.openId4VcIssuerService = openId4VcIssuerService - this.config = config - } + public readonly config: OpenId4VcIssuerModuleConfig, + private agentContext: AgentContext, + private openId4VcIssuerService: OpenId4VcIssuerService + ) {} public async getAllIssuers() { return this.openId4VcIssuerService.getAllIssuers(this.agentContext) @@ -83,7 +71,7 @@ export class OpenId4VcIssuerApi { * @returns Object containing the payload of the credential offer and the credential offer request, which can be sent to the wallet. */ public async createCredentialOffer( - options: CreateCredentialOfferOptions & { issuerId: string } + options: OpenId4VciCreateCredentialOfferOptions & { issuerId: string } ): Promise { const { issuerId, ...rest } = options const issuer = await this.openId4VcIssuerService.getByIssuerId(this.agentContext, issuerId) diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts index e3670876bc..b02363dc8a 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts @@ -1,18 +1,16 @@ import type { OpenId4VcIssuerModuleConfigOptions } from './OpenId4VcIssuerModuleConfig' -import type { IssuanceRequest } from './router/requestContext' +import type { OpenId4VcIssuanceRequest } from './router' import type { AgentContext, DependencyManager, Module } from '@aries-framework/core' import { AgentConfig } from '@aries-framework/core' -import { getRequestContext } from '../shared/router' +import { getAgentContextForActorId, getRequestContext, importExpress } from '../shared/router' import { OpenId4VcIssuerApi } from './OpenId4VcIssuerApi' import { OpenId4VcIssuerModuleConfig } from './OpenId4VcIssuerModuleConfig' import { OpenId4VcIssuerService } from './OpenId4VcIssuerService' import { OpenId4VcIssuerRepository } from './repository/OpenId4VcIssuerRepository' import { configureAccessTokenEndpoint, configureCredentialEndpoint, configureIssuerMetadataEndpoint } from './router' -import { importExpress } from './router/express' -import { getAgentContextForIssuerId } from './router/requestContext' /** * @public @@ -69,7 +67,7 @@ export class OpenId4VcIssuerModule implements Module { // parse application/json contextRouter.use(json()) - contextRouter.param('issuerId', async (req: IssuanceRequest, _res, next, issuerId: string) => { + contextRouter.param('issuerId', async (req: OpenId4VcIssuanceRequest, _res, next, issuerId: string) => { if (!issuerId) { _res.status(404).send('Not found') } @@ -77,7 +75,8 @@ export class OpenId4VcIssuerModule implements Module { let agentContext: AgentContext | undefined = undefined try { - agentContext = await getAgentContextForIssuerId(rootAgentContext, issuerId) + // FIXME: should we create combined openId actor record? + agentContext = await getAgentContextForActorId(rootAgentContext, issuerId) const issuerApi = agentContext.dependencyManager.resolve(OpenId4VcIssuerApi) const issuer = await issuerApi.getByIssuerId(issuerId) @@ -108,7 +107,7 @@ export class OpenId4VcIssuerModule implements Module { configureCredentialEndpoint(endpointRouter, this.config.credentialEndpoint) // FIXME: Will this be called when an error occurs / 404 is returned earlier on? - contextRouter.use(async (req: IssuanceRequest, _res, next) => { + contextRouter.use(async (req: OpenId4VcIssuanceRequest, _res, next) => { const { agentContext } = getRequestContext(req) await agentContext.endSession() next() diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts index aebc8f9915..71157cbe0e 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts @@ -1,11 +1,11 @@ -import type { AccessTokenEndpointConfig, CredentialEndpointConfig } from './OpenId4VcIssuerServiceOptions' -import type { AgentContext } from '@aries-framework/core' +import type { AccessTokenEndpointConfig, CredentialEndpointConfig } from './router' +import type { AgentContext, Optional } from '@aries-framework/core' import type { CNonceState, CredentialOfferSession, IStateManager, StateType, URIState } from '@sphereon/oid4vci-common' import type { Router } from 'express' import { MemoryStates } from '@sphereon/oid4vci-issuer' -import { importExpress } from './router/express' +import { importExpress } from '../shared/router' export type StateManagerFactory = () => IStateManager @@ -45,14 +45,14 @@ export interface OpenId4VcIssuerModuleConfigOptions { uriStateManagerFactory?: StateManagerFactory } -type Optional = Omit & Partial> - export class OpenId4VcIssuerModuleConfig { private options: OpenId4VcIssuerModuleConfigOptions - private uriStateManagerMap: Map> + public readonly router: Router + + // FIXME: remove private credentialOfferSessionManagerMap: Map> + private uriStateManagerMap: Map> private cNonceStateManagerMap: Map> - public readonly router: Router public constructor(options: OpenId4VcIssuerModuleConfigOptions) { this.uriStateManagerMap = new Map() diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts index d4ec3c54f0..037d004d66 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts @@ -1,14 +1,14 @@ import type { - AuthorizationCodeFlowConfig, - CreateCredentialOfferOptions, + OpenId4VciAuthorizationCodeFlowConfig, + OpenId4VciCreateCredentialOfferOptions, CreateCredentialResponseOptions, - CreateIssuerOptions, + OpenId4VciCreateIssuerOptions, CredentialOffer, - IssuerMetadata, + OpenId4VcIssuerMetadata, OpenId4VciSignCredential, OpenId4VciSignSdJwtCredential, OpenId4VciSignW3cCredential, - PreAuthorizedCodeFlowConfig, + OpenId4VciPreAuthorizedCodeFlowConfig, } from './OpenId4VcIssuerServiceOptions' import type { ReferencedOfferedCredentialWithMetadata } from '../openid4vc-holder/reception/utils/IssuerMetadataUtils' import type { CredentialHolderBinding } from '../shared' @@ -57,13 +57,13 @@ import { OfferedCredentialType, getOfferedCredentialsWithMetadata, } from '../openid4vc-holder/reception/utils/IssuerMetadataUtils' +import { storeActorIdForContextCorrelationId } from '../shared/router' import { getSphereonW3cVerifiableCredential } from '../shared/transform' import { getProofTypeFromKey } from '../shared/utils' import { OpenId4VcIssuerModuleConfig } from './OpenId4VcIssuerModuleConfig' import { OpenId4VcIssuerRecord } from './repository/OpenId4VcIssuerRecord' import { OpenId4VcIssuerRepository } from './repository/OpenId4VcIssuerRepository' -import { storeIssuerIdForContextCorrelationId } from './router/requestContext' const w3cOpenId4VcFormats = [ OpenId4VciCredentialFormatProfile.JwtVcJson, @@ -93,23 +93,24 @@ export class OpenId4VcIssuerService { this.openId4VcIssuerRepository = openId4VcIssuerRepository } - public getIssuerMetadata(agentContext: AgentContext, issuerRecord: OpenId4VcIssuerRecord): IssuerMetadata { + public getIssuerMetadata(agentContext: AgentContext, issuerRecord: OpenId4VcIssuerRecord): OpenId4VcIssuerMetadata { const config = agentContext.dependencyManager.resolve(OpenId4VcIssuerModuleConfig) const issuerUrl = joinUriParts(config.baseUrl, [issuerRecord.issuerId]) + const issuerMetadata = { issuerUrl, tokenEndpoint: joinUriParts(issuerUrl, [config.accessTokenEndpoint.endpointPath]), credentialEndpoint: joinUriParts(issuerUrl, [config.credentialEndpoint.endpointPath]), credentialsSupported: issuerRecord.credentialsSupported, issuerDisplay: issuerRecord.display, - } satisfies IssuerMetadata + } satisfies OpenId4VcIssuerMetadata return issuerMetadata } public async createCredentialOffer( agentContext: AgentContext, - options: CreateCredentialOfferOptions & { issuer: OpenId4VcIssuerRecord } + options: OpenId4VciCreateCredentialOfferOptions & { issuer: OpenId4VcIssuerRecord } ): Promise { const { preAuthorizedCodeFlowConfig, authorizationCodeFlowConfig, issuer, offeredCredentials } = options @@ -190,7 +191,7 @@ export class OpenId4VcIssuerService { return this.openId4VcIssuerRepository.update(agentContext, issuer) } - public async createIssuer(agentContext: AgentContext, options: CreateIssuerOptions) { + public async createIssuer(agentContext: AgentContext, options: OpenId4VciCreateIssuerOptions) { // TODO: ideally we can store additional data with a key, such as: // - createdAt // - purpose @@ -205,7 +206,7 @@ export class OpenId4VcIssuerService { }) await this.openId4VcIssuerRepository.save(agentContext, openId4VcIssuer) - await storeIssuerIdForContextCorrelationId(agentContext, openId4VcIssuer.issuerId) + await storeActorIdForContextCorrelationId(agentContext, openId4VcIssuer.issuerId) return openId4VcIssuer } @@ -306,8 +307,8 @@ export class OpenId4VcIssuerService { private async getGrantsFromConfig( agentContext: AgentContext, - preAuthorizedCodeFlowConfig?: PreAuthorizedCodeFlowConfig, - authorizationCodeFlowConfig?: AuthorizationCodeFlowConfig + preAuthorizedCodeFlowConfig?: OpenId4VciPreAuthorizedCodeFlowConfig, + authorizationCodeFlowConfig?: OpenId4VciAuthorizationCodeFlowConfig ) { if (!preAuthorizedCodeFlowConfig && !authorizationCodeFlowConfig) { throw new AriesFrameworkError( diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts index d47eb147e9..351005befc 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts @@ -10,16 +10,16 @@ import type { AgentContext, W3cCredential } from '@aries-framework/core' import type { SdJwtVcSignOptions } from '@aries-framework/sd-jwt-vc' import type { CredentialOfferPayloadV1_0_11 } from '@sphereon/oid4vci-common' -export type PreAuthorizedCodeFlowConfig = { +export interface OpenId4VciPreAuthorizedCodeFlowConfig { preAuthorizedCode?: string userPinRequired?: boolean } -export type AuthorizationCodeFlowConfig = { +export type OpenId4VciAuthorizationCodeFlowConfig = { issuerState?: string } -export type IssuerMetadata = { +export type OpenId4VcIssuerMetadata = { // The Credential Issuer's identifier. (URL using the https scheme) issuerUrl: string credentialEndpoint: string @@ -30,9 +30,9 @@ export type IssuerMetadata = { credentialsSupported: OpenId4VciCredentialSupported[] } -export type CreateIssuerOptions = Pick +export type OpenId4VciCreateIssuerOptions = Pick -export interface CreateCredentialOfferOptions { +export interface OpenId4VciCreateCredentialOfferOptions { // NOTE: v11 of OID4VCI supports both inline and referenced (to credentials_supported.id) credential offers. // In draft 12 the inline credential offers have been removed and to make the migration to v12 easier // we only support referenced credentials in an offer @@ -45,8 +45,8 @@ export interface CreateCredentialOfferOptions { // The base URI of the credential offer uri baseUri?: string - preAuthorizedCodeFlowConfig?: PreAuthorizedCodeFlowConfig - authorizationCodeFlowConfig?: AuthorizationCodeFlowConfig + preAuthorizedCodeFlowConfig?: OpenId4VciPreAuthorizedCodeFlowConfig + authorizationCodeFlowConfig?: OpenId4VciAuthorizationCodeFlowConfig credentialOfferUri?: string } @@ -58,6 +58,7 @@ export type CredentialOffer = { credentialOfferUri: string } +// FIXME: openid4vc prefix for all interfaces export interface CreateCredentialResponseOptions { credentialRequest: OpenId4VciCredentialRequest @@ -71,70 +72,11 @@ export interface CreateCredentialResponseOptions { credential?: OpenId4VciSignCredential } -export type MetadataEndpointConfig = { - /** - * Configures the router to expose the metadata endpoint. - */ - enabled: true -} - -export type AccessTokenEndpointConfig = { - /** - * The path at which the token endpoint should be made available. Note that it will be - * hosted at a subpath to take into account multiple tenants and issuers. - * - * @default /token - */ - endpointPath: string - - // FIXME: rename, more specific - /** - * The minimum amount of time in seconds that the client SHOULD wait between polling requests to the Token Endpoint in the Pre-Authorized Code Flow. - * If no value is provided, clients MUST use 5 as the default. - */ - interval?: number - - /** - * The maximum amount of time in seconds that the pre-authorized code is valid. - * @default 360 (5 minutes) // FIXME: what should be the default value - */ - preAuthorizedCodeExpirationInSeconds: number - - /** - * The time after which the cNonce from the access token response will - * expire. - * - * @default 360 (5 minutes) // FIXME: what should be the default value? - */ - cNonceExpiresInSeconds: number - - /** - * The time after which the token will expire. - * - * @default 360 (5 minutes) // FIXME: what should be the default value? - */ - tokenExpiresInSeconds: number -} - -export type CredentialEndpointConfig = { - /** - * The path at which the credential endpoint should be made available. Note that it will be - * hosted at a subpath to take into account multiple tenants and issuers. - * - * @default /credential - */ - endpointPath: string - - /** - * A function mapping a credential request to the credential to be issued. - */ - credentialRequestToCredentialMapper: CredentialRequestToCredentialMapper -} - +// FIXME: openid4vc prefix for all interfaces // FIXME: Flows: // - provide credential data at time of offer creation // - provide credential data dynamically using this method -export type CredentialRequestToCredentialMapper = (options: { +export type OpenId4VciCredentialRequestToCredentialMapper = (options: { agentContext: AgentContext /** diff --git a/packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts b/packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts index f77cf5be57..ea54ad6e4b 100644 --- a/packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts @@ -1,5 +1,4 @@ -import type { IssuanceRequest } from './requestContext' -import type { AccessTokenEndpointConfig } from '../OpenId4VcIssuerServiceOptions' +import type { OpenId4VcIssuanceRequest } from './requestContext' import type { AgentContext } from '@aries-framework/core' import type { JWTSignerCallback } from '@sphereon/oid4vci-common' import type { NextFunction, Response, Router } from 'express' @@ -24,7 +23,54 @@ import { getRequestContext, sendErrorResponse } from '../../shared/router' import { OpenId4VcIssuerModuleConfig } from '../OpenId4VcIssuerModuleConfig' import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' -const getJwtSignerCallback = (agentContext: AgentContext, signerPublicKey: Key): JWTSignerCallback => { +export interface AccessTokenEndpointConfig { + /** + * The path at which the token endpoint should be made available. Note that it will be + * hosted at a subpath to take into account multiple tenants and issuers. + * + * @default /token + */ + endpointPath: string + + // FIXME: rename, more specific + /** + * The minimum amount of time in seconds that the client SHOULD wait between polling requests to the Token Endpoint in the Pre-Authorized Code Flow. + * If no value is provided, clients MUST use 5 as the default. + */ + interval?: number + + /** + * The maximum amount of time in seconds that the pre-authorized code is valid. + * @default 360 (5 minutes) // FIXME: what should be the default value + */ + preAuthorizedCodeExpirationInSeconds: number + + /** + * The time after which the cNonce from the access token response will + * expire. + * + * @default 360 (5 minutes) // FIXME: what should be the default value? + */ + cNonceExpiresInSeconds: number + + /** + * The time after which the token will expire. + * + * @default 360 (5 minutes) // FIXME: what should be the default value? + */ + tokenExpiresInSeconds: number +} + +export function configureAccessTokenEndpoint(router: Router, config: AccessTokenEndpointConfig) { + const { preAuthorizedCodeExpirationInSeconds } = config + router.post( + config.endpointPath, + verifyTokenRequest({ preAuthorizedCodeExpirationInSeconds }), + handleTokenRequest(config) + ) +} + +function getJwtSignerCallback(agentContext: AgentContext, signerPublicKey: Key): JWTSignerCallback { return async (jwt, _kid) => { if (_kid) { throw new AriesFrameworkError('Kid should not be supplied externally.') @@ -51,10 +97,10 @@ const getJwtSignerCallback = (agentContext: AgentContext, signerPublicKey: Key): } } -export const handleTokenRequest = (config: AccessTokenEndpointConfig) => { +export function handleTokenRequest(config: AccessTokenEndpointConfig) { const { tokenExpiresInSeconds, cNonceExpiresInSeconds, interval } = config - return async (request: IssuanceRequest, response: Response) => { + return async (request: OpenId4VcIssuanceRequest, response: Response) => { response.set({ 'Cache-Control': 'no-store', Pragma: 'no-cache' }) const requestContext = getRequestContext(request) @@ -90,9 +136,9 @@ export const handleTokenRequest = (config: AccessTokenEndpointConfig) => { } } -export const verifyTokenRequest = (options: { preAuthorizedCodeExpirationInSeconds: number }) => { +export function verifyTokenRequest(options: { preAuthorizedCodeExpirationInSeconds: number }) { const { preAuthorizedCodeExpirationInSeconds } = options - return async (request: IssuanceRequest, response: Response, next: NextFunction) => { + return async (request: OpenId4VcIssuanceRequest, response: Response, next: NextFunction) => { const { agentContext } = getRequestContext(request) const openId4VcIssuerConfig = agentContext.dependencyManager.resolve(OpenId4VcIssuerModuleConfig) @@ -119,12 +165,3 @@ export const verifyTokenRequest = (options: { preAuthorizedCodeExpirationInSecon return next() } } - -export function configureAccessTokenEndpoint(router: Router, config: AccessTokenEndpointConfig) { - const { preAuthorizedCodeExpirationInSeconds } = config - router.post( - config.endpointPath, - verifyTokenRequest({ preAuthorizedCodeExpirationInSeconds }), - handleTokenRequest(config) - ) -} diff --git a/packages/openid4vc/src/openid4vc-issuer/router/credentialEndpoint.ts b/packages/openid4vc/src/openid4vc-issuer/router/credentialEndpoint.ts index 36fe74c421..f8c0dcaae4 100644 --- a/packages/openid4vc/src/openid4vc-issuer/router/credentialEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-issuer/router/credentialEndpoint.ts @@ -1,15 +1,29 @@ -import type { IssuanceRequest } from './requestContext' -import type { CredentialEndpointConfig } from '../OpenId4VcIssuerServiceOptions' +import type { OpenId4VcIssuanceRequest } from './requestContext' +import type { OpenId4VciCredentialRequestToCredentialMapper } from '../OpenId4VcIssuerServiceOptions' import type { CredentialRequestV1_0_11 } from '@sphereon/oid4vci-common' import type { Router, Response } from 'express' import { getRequestContext, sendErrorResponse } from '../../shared/router' import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' +export interface CredentialEndpointConfig { + /** + * The path at which the credential endpoint should be made available. Note that it will be + * hosted at a subpath to take into account multiple tenants and issuers. + * + * @default /credential + */ + endpointPath: string + + /** + * A function mapping a credential request to the credential to be issued. + */ + credentialRequestToCredentialMapper: OpenId4VciCredentialRequestToCredentialMapper +} + export function configureCredentialEndpoint(router: Router, config: CredentialEndpointConfig) { - router.post(config.endpointPath, async (request: IssuanceRequest, response: Response) => { - const requestContext = getRequestContext(request) - const { agentContext, issuer } = requestContext + router.post(config.endpointPath, async (request: OpenId4VcIssuanceRequest, response: Response) => { + const { agentContext, issuer } = getRequestContext(request) const openId4VcIssuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService) try { @@ -20,8 +34,8 @@ export function configureCredentialEndpoint(router: Router, config: CredentialEn }) return response.send(issueCredentialResponse) - } catch (e) { - sendErrorResponse(response, agentContext.config.logger, 500, 'invalid_request', e) + } catch (error) { + sendErrorResponse(response, agentContext.config.logger, 500, 'invalid_request', error) } }) } diff --git a/packages/openid4vc/src/openid4vc-issuer/router/index.ts b/packages/openid4vc/src/openid4vc-issuer/router/index.ts index 836352a1d7..4d8bae100e 100644 --- a/packages/openid4vc/src/openid4vc-issuer/router/index.ts +++ b/packages/openid4vc/src/openid4vc-issuer/router/index.ts @@ -1,4 +1,4 @@ -export { configureAccessTokenEndpoint } from './accessTokenEndpoint' -export { configureCredentialEndpoint } from './credentialEndpoint' +export { configureAccessTokenEndpoint, AccessTokenEndpointConfig } from './accessTokenEndpoint' +export { configureCredentialEndpoint, CredentialEndpointConfig } from './credentialEndpoint' export { configureIssuerMetadataEndpoint } from './metadataEndpoint' -export { IssuanceRequest, IssuanceRequestContext } from './requestContext' +export { OpenId4VcIssuanceRequest } from './requestContext' diff --git a/packages/openid4vc/src/openid4vc-issuer/router/metadataEndpoint.ts b/packages/openid4vc/src/openid4vc-issuer/router/metadataEndpoint.ts index a2a698f169..db395fe0b8 100644 --- a/packages/openid4vc/src/openid4vc-issuer/router/metadataEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-issuer/router/metadataEndpoint.ts @@ -1,4 +1,4 @@ -import type { IssuanceRequest } from './requestContext' +import type { OpenId4VcIssuanceRequest } from './requestContext' import type { CredentialIssuerMetadata } from '@sphereon/oid4vci-common' import type { Router, Response } from 'express' @@ -6,7 +6,7 @@ import { getRequestContext, sendErrorResponse } from '../../shared/router' import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' export function configureIssuerMetadataEndpoint(router: Router) { - router.get('/.well-known/openid-credential-issuer', (_request: IssuanceRequest, response: Response) => { + router.get('/.well-known/openid-credential-issuer', (_request: OpenId4VcIssuanceRequest, response: Response) => { const { agentContext, issuer } = getRequestContext(_request) const openId4VcIssuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService) diff --git a/packages/openid4vc/src/openid4vc-issuer/router/requestContext.ts b/packages/openid4vc/src/openid4vc-issuer/router/requestContext.ts index 36a4e466c8..69e0caadb3 100644 --- a/packages/openid4vc/src/openid4vc-issuer/router/requestContext.ts +++ b/packages/openid4vc/src/openid4vc-issuer/router/requestContext.ts @@ -1,66 +1,4 @@ -import type { RequestContext } from '../../shared/router' -import type { OpenId4VcIssuerRecord } from '../repository/OpenId4VcIssuerRecord' -import type { AgentContext, AgentContextProvider } from '@aries-framework/core' -import type { TenantsModule } from '@aries-framework/tenants' -import type { Request } from 'express' +import type { OpenId4VcRequest } from '../../shared/router' +import type { OpenId4VcIssuerRecord } from '../repository' -import { InjectionSymbols, getApiForModuleByName } from '@aries-framework/core' - -// Type is currently same as base request context -export type IssuanceRequestContext = RequestContext & { issuer: OpenId4VcIssuerRecord } -export interface IssuanceRequest extends Request { - requestContext?: IssuanceRequestContext -} - -const OPENID4VC_ISSUER_IDS_METADATA_KEY = '_openid4vc/openId4VcIssuerIds' - -export async function getAgentContextForIssuerId(rootAgentContext: AgentContext, issuerId: string) { - // Check if multi-tenancy is enabled, and if so find the associated multi-tenant record - // This is a bit hacky as it uses the tenants module to store the openid4vc issuer id - // but this way we don't have to expose the contextCorrelationId in the issuer metadata - const tenantsApi = getApiForModuleByName(rootAgentContext, 'TenantsModule') - if (tenantsApi) { - const [tenant] = await tenantsApi.findTenantsByQuery({ - [OPENID4VC_ISSUER_IDS_METADATA_KEY]: [issuerId], - }) - - if (tenant) { - const agentContextProvider = rootAgentContext.dependencyManager.resolve( - InjectionSymbols.AgentContextProvider - ) - return agentContextProvider.getAgentContextForContextCorrelationId(tenant.id) - } - } - - return rootAgentContext -} - -/** - * Store the issuer id associated with a context correlation id. If multi-tenancy is not used - * this method won't do anything as we can just use the issuer from the default context. However - * if multi-tenancy is used, we will store the issuer id in the tenant record metadata so it can - * be queried when a request comes in for the specific issuer id. - * - * The reason for doing this is that we don't want to expose the context correlation id in the - * issuer metadata url, as it is then possible to see exactly which issuers are registered under - * the same agent. - */ -export async function storeIssuerIdForContextCorrelationId(agentContext: AgentContext, issuerId: string) { - // It's kind of hacky, but we add support for the tenants module specifically here to map an issuerId to - // a specific tenant. Otherwise we have to expose /:contextCorrelationId/:issuerId in all the public URLs - // which is of course not so nice. - // FIXME: it's maybe nicer to just depend on the tenants module - const tenantsApi = getApiForModuleByName(agentContext, 'TenantsModule') - - // We don't want to query the tenant record if the current context is the root context - if (tenantsApi && tenantsApi.rootAgentContext.contextCorrelationId !== agentContext.contextCorrelationId) { - const tenantRecord = await tenantsApi.getTenantById(agentContext.contextCorrelationId) - - const currentOpenId4VcIssuerIds = tenantRecord.metadata.get(OPENID4VC_ISSUER_IDS_METADATA_KEY) ?? [] - const openId4VcIssuerIds = [...currentOpenId4VcIssuerIds, issuerId] - - tenantRecord.metadata.set(OPENID4VC_ISSUER_IDS_METADATA_KEY, openId4VcIssuerIds) - tenantRecord.setTag(OPENID4VC_ISSUER_IDS_METADATA_KEY, openId4VcIssuerIds) - await tenantsApi.updateTenant(tenantRecord) - } -} +export type OpenId4VcIssuanceRequest = OpenId4VcRequest<{ issuer: OpenId4VcIssuerRecord }> diff --git a/packages/openid4vc/src/shared/router.ts b/packages/openid4vc/src/shared/router/context.ts similarity index 67% rename from packages/openid4vc/src/shared/router.ts rename to packages/openid4vc/src/shared/router/context.ts index 5073f7a348..bfd0afacc6 100644 --- a/packages/openid4vc/src/shared/router.ts +++ b/packages/openid4vc/src/shared/router/context.ts @@ -3,7 +3,11 @@ import type { Response, Request } from 'express' import { AriesFrameworkError } from '@aries-framework/core' -export interface RequestContext { +export interface OpenId4VcRequest = Record> extends Request { + requestContext?: RC & OpenId4VcRequestContext +} + +export interface OpenId4VcRequestContext { agentContext: AgentContext } @@ -19,9 +23,8 @@ export function sendErrorResponse(response: Response, logger: Logger, code: numb return response.status(code).json(body) } -export function getRequestContext( - request: T -): NonNullable { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function getRequestContext>(request: T): NonNullable { const requestContext = request.requestContext if (!requestContext) throw new AriesFrameworkError('Request context not set.') diff --git a/packages/openid4vc/src/openid4vc-issuer/router/express.ts b/packages/openid4vc/src/shared/router/express.ts similarity index 100% rename from packages/openid4vc/src/openid4vc-issuer/router/express.ts rename to packages/openid4vc/src/shared/router/express.ts diff --git a/packages/openid4vc/src/shared/router/index.ts b/packages/openid4vc/src/shared/router/index.ts new file mode 100644 index 0000000000..dc3697dcc1 --- /dev/null +++ b/packages/openid4vc/src/shared/router/index.ts @@ -0,0 +1,3 @@ +export * from './express' +export * from './context' +export * from './tenants' diff --git a/packages/openid4vc/src/shared/router/tenants.ts b/packages/openid4vc/src/shared/router/tenants.ts new file mode 100644 index 0000000000..93f92164f8 --- /dev/null +++ b/packages/openid4vc/src/shared/router/tenants.ts @@ -0,0 +1,57 @@ +import type { AgentContext, AgentContextProvider } from '@aries-framework/core' +import type { TenantsModule } from '@aries-framework/tenants' + +import { getApiForModuleByName, InjectionSymbols } from '@aries-framework/core' + +const OPENID4VC_ACTOR_IDS_METADATA_KEY = '_openid4vc/openId4VcActorIds' + +export async function getAgentContextForActorId(rootAgentContext: AgentContext, actorId: string) { + // Check if multi-tenancy is enabled, and if so find the associated multi-tenant record + // This is a bit hacky as it uses the tenants module to store the openid4vc actor id + // but this way we don't have to expose the contextCorrelationId in the openid metadata + const tenantsApi = getApiForModuleByName(rootAgentContext, 'TenantsModule') + if (tenantsApi) { + const [tenant] = await tenantsApi.findTenantsByQuery({ + [OPENID4VC_ACTOR_IDS_METADATA_KEY]: [actorId], + }) + + if (tenant) { + const agentContextProvider = rootAgentContext.dependencyManager.resolve( + InjectionSymbols.AgentContextProvider + ) + return agentContextProvider.getAgentContextForContextCorrelationId(tenant.id) + } + } + + return rootAgentContext +} + +/** + * Store the actor id associated with a context correlation id. If multi-tenancy is not used + * this method won't do anything as we can just use the actor from the default context. However + * if multi-tenancy is used, we will store the actor id in the tenant record metadata so it can + * be queried when a request comes in for the specific actor id. + * + * The reason for doing this is that we don't want to expose the context correlation id in the + * actor metadata url, as it is then possible to see exactly which actors are registered under + * the same agent. + */ +export async function storeActorIdForContextCorrelationId(agentContext: AgentContext, actorId: string) { + // It's kind of hacky, but we add support for the tenants module specifically here to map an actorId to + // a specific tenant. Otherwise we have to expose /:contextCorrelationId/:actorId in all the public URLs + // which is of course not so nice. + // FIXME: it's maybe nicer to just depend on the tenants module + const tenantsApi = getApiForModuleByName(agentContext, 'TenantsModule') + + // We don't want to query the tenant record if the current context is the root context + if (tenantsApi && tenantsApi.rootAgentContext.contextCorrelationId !== agentContext.contextCorrelationId) { + const tenantRecord = await tenantsApi.getTenantById(agentContext.contextCorrelationId) + + const currentOpenId4VcActorIds = tenantRecord.metadata.get(OPENID4VC_ACTOR_IDS_METADATA_KEY) ?? [] + const openId4VcActorIds = [...currentOpenId4VcActorIds, actorId] + + tenantRecord.metadata.set(OPENID4VC_ACTOR_IDS_METADATA_KEY, openId4VcActorIds) + tenantRecord.setTag(OPENID4VC_ACTOR_IDS_METADATA_KEY, openId4VcActorIds) + await tenantsApi.updateTenant(tenantRecord) + } +} diff --git a/packages/openid4vc/src/shared/utils.ts b/packages/openid4vc/src/shared/utils.ts index 40e698105c..5351a7e48a 100644 --- a/packages/openid4vc/src/shared/utils.ts +++ b/packages/openid4vc/src/shared/utils.ts @@ -86,11 +86,6 @@ export function getResolver(agentContext: AgentContext) { } } -export async function generateRandomValues(agentContext: AgentContext, count: number) { - const randomValuesPromises = Array.from({ length: count }, () => agentContext.wallet.generateNonce()) - return await Promise.all(randomValuesPromises) -} - export const getProofTypeFromKey = (agentContext: AgentContext, key: Key) => { const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) From b74370b48922fd1aa76d82422391a800d6e16821 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sun, 21 Jan 2024 15:36:08 +0700 Subject: [PATCH 101/115] refactored a bunch of code Signed-off-by: Timo Glastra --- .../openid4vc-holder/OpenId4VcHolderApi.ts | 15 +- .../openid4vc-holder/OpenId4VcHolderModule.ts | 4 +- .../OpenId4VciHolderService.ts | 279 ++++++------------ .../OpenId4VciHolderServiceOptions.ts | 30 +- .../OpenId4VpHolderService.ts | 21 +- .../OpenId4VpHolderServiceOptions.ts | 0 .../openid4vc-holder/presentation/index.ts | 2 - .../src/openid4vc-holder/reception/index.ts | 3 - .../reception/utils/Formats.ts | 45 --- .../reception/utils/IssuerMetadataUtils.ts | 276 ----------------- .../__tests__/claimFormatMapping.test.ts | 41 --- .../reception/utils/claimFormatMapping.ts | 40 --- .../openid4vc-holder/reception/utils/index.ts | 3 - .../openid4vc-issuer/OpenId4VcIssuerApi.ts | 6 +- .../OpenId4VcIssuerService.ts | 101 ++++--- .../OpenId4VcIssuerServiceOptions.ts | 6 +- .../repository/OpenId4VcIssuerRecord.ts | 9 +- .../router/credentialEndpoint.ts | 4 +- .../OpenId4VcVerifierApi.ts | 99 ++++--- .../OpenId4VcVerifierModule.ts | 75 ++++- .../OpenId4VcVerifierModuleConfig.ts | 62 ++-- .../OpenId4VcVerifierServiceOptions.ts | 122 ++++---- .../repository/OpenId4VcVerifierRecord.ts | 44 +++ .../repository/OpenId4VcVerifierRepository.ts | 23 ++ .../openid4vc-verifier/repository/index.ts | 2 + .../router/OpenId4VpEndpointConfiguration.ts | 47 --- .../router/authorizationEndpoint.ts | 36 +++ .../src/openid4vc-verifier/router/index.ts | 2 + .../router/requestContext.ts | 4 + .../staticOpConfiguration.ts | 26 ++ packages/openid4vc/src/shared/index.ts | 1 + .../src/shared/issuerMetadataUtils.ts | 83 ++++++ .../OpenId4VciCredentialFormatProfile.ts | 6 + packages/openid4vc/src/shared/models/index.ts | 5 + 34 files changed, 638 insertions(+), 884 deletions(-) rename packages/openid4vc/src/openid4vc-holder/{reception => }/OpenId4VciHolderService.ts (72%) rename packages/openid4vc/src/openid4vc-holder/{reception => }/OpenId4VciHolderServiceOptions.ts (90%) rename packages/openid4vc/src/openid4vc-holder/{presentation => }/OpenId4VpHolderService.ts (94%) rename packages/openid4vc/src/openid4vc-holder/{presentation => }/OpenId4VpHolderServiceOptions.ts (100%) delete mode 100644 packages/openid4vc/src/openid4vc-holder/presentation/index.ts delete mode 100644 packages/openid4vc/src/openid4vc-holder/reception/index.ts delete mode 100644 packages/openid4vc/src/openid4vc-holder/reception/utils/Formats.ts delete mode 100644 packages/openid4vc/src/openid4vc-holder/reception/utils/IssuerMetadataUtils.ts delete mode 100644 packages/openid4vc/src/openid4vc-holder/reception/utils/__tests__/claimFormatMapping.test.ts delete mode 100644 packages/openid4vc/src/openid4vc-holder/reception/utils/claimFormatMapping.ts delete mode 100644 packages/openid4vc/src/openid4vc-holder/reception/utils/index.ts create mode 100644 packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcVerifierRecord.ts create mode 100644 packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcVerifierRepository.ts create mode 100644 packages/openid4vc/src/openid4vc-verifier/repository/index.ts delete mode 100644 packages/openid4vc/src/openid4vc-verifier/router/OpenId4VpEndpointConfiguration.ts create mode 100644 packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts create mode 100644 packages/openid4vc/src/openid4vc-verifier/router/index.ts create mode 100644 packages/openid4vc/src/openid4vc-verifier/router/requestContext.ts create mode 100644 packages/openid4vc/src/openid4vc-verifier/staticOpConfiguration.ts create mode 100644 packages/openid4vc/src/shared/issuerMetadataUtils.ts create mode 100644 packages/openid4vc/src/shared/models/OpenId4VciCredentialFormatProfile.ts diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts index 6f0f00cdfe..d58c1c17be 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts @@ -1,17 +1,16 @@ -import type { AuthenticationRequest, PresentationRequest } from './presentation' import type { ResolvedCredentialOffer, ResolvedAuthorizationRequest, AuthCodeFlowOptions, AcceptCredentialOfferOptions, - CredentialOfferPayloadV1_0_11, -} from './reception' +} from './OpenId4VciHolderServiceOptions' +import type { AuthenticationRequest, PresentationRequest } from './OpenId4VpHolderServiceOptions' import type { VerificationMethod, DifPexInputDescriptorToCredentials } from '@aries-framework/core' import { injectable, AgentContext } from '@aries-framework/core' -import { OpenId4VpHolderService } from './presentation' -import { OpenId4VciHolderService } from './reception' +import { OpenId4VciHolderService } from './OpenId4VciHolderService' +import { OpenId4VpHolderService } from './OpenId4VpHolderService' // FIXME: the holder API is not really consistent with the issuer API // FIXME: it's not immediately clear which methods are for receiving vc proving @@ -82,13 +81,13 @@ export class OpenId4VcHolderApi { } /** - * Resolves a credential offer given as payload, credential offer URL, or issuance initiation URL, + * Resolves a credential offer given as credential offer URL, or issuance initiation URL, * into a unified format with metadata. * * @param credentialOffer the credential offer to resolve * @returns The uniform credential offer payload, the issuer metadata, protocol version, and the offered credentials with metadata. */ - public async resolveCredentialOffer(credentialOffer: string | CredentialOfferPayloadV1_0_11) { + public async resolveCredentialOffer(credentialOffer: string) { return await this.openId4VciHolderService.resolveCredentialOffer(credentialOffer) } @@ -99,7 +98,7 @@ export class OpenId4VcHolderApi { * * Authorization to request credentials can be requested via authorization_details or scopes. * This function automatically generates the authorization_details for all offered credentials. - * If scopes are provided, the provided scopes are send alongside the authorization_details. + * If scopes are provided, the provided scopes are sent alongside the authorization_details. * * @param resolvedCredentialOffer Obtained through @see resolveCredentialOffer * @param authCodeFlowOptions diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderModule.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderModule.ts index f8ff96e115..b610e2ffe5 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderModule.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderModule.ts @@ -3,8 +3,8 @@ import type { DependencyManager, Module } from '@aries-framework/core' import { AgentConfig } from '@aries-framework/core' import { OpenId4VcHolderApi } from './OpenId4VcHolderApi' -import { OpenId4VpHolderService } from './presentation' -import { OpenId4VciHolderService } from './reception' +import { OpenId4VciHolderService } from './OpenId4VciHolderService' +import { OpenId4VpHolderService } from './OpenId4VpHolderService' /** * @public @module OpenId4VcHolderModule diff --git a/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts similarity index 72% rename from packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderService.ts rename to packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts index e4b6080c3a..0a6f5223c7 100644 --- a/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts @@ -1,16 +1,19 @@ -import type { OfferedCredentialWithMetadata } from './utils/IssuerMetadataUtils' +import type { + OpenId4VciCredentialOfferPayload, + OpenId4VciCredentialSupported, + OpenId4VciCredentialSupportedWithId, + OpenId4VciIssuerMetadata, +} from '../shared' import type { AgentContext, JwaSignatureAlgorithm, W3cVerifiableCredential, Key, JwkJson } from '@aries-framework/core' import type { SdJwtVcModule, SdJwtVc } from '@aries-framework/sd-jwt-vc' import type { AccessTokenResponse, - CredentialOfferPayloadV1_0_11, CredentialResponse, - EndpointMetadataResult, Jwt, OpenIDResponse, PushedAuthorizationResponse, - UniformCredentialOfferPayload, AuthorizationDetails, + AuthorizationDetailsJwtVcJson, } from '@sphereon/oid4vci-common' import { @@ -34,26 +37,24 @@ import { inject, injectable, parseDid, - equalsIgnoreOrder, getApiForModuleByName, } from '@aries-framework/core' import { AccessTokenClient, - CredentialOfferClient, CredentialRequestClientBuilder, ProofOfPossessionBuilder, formPost, + OpenID4VCIClient, } from '@sphereon/oid4vci-client' -import { - OpenId4VCIVersion, - CodeChallengeMethod, - assertedUniformCredentialOffer, - ResponseType, - convertJsonToURI, - JsonURIMode, -} from '@sphereon/oid4vci-common' +import { CodeChallengeMethod, ResponseType, convertJsonToURI, JsonURIMode } from '@sphereon/oid4vci-common' -import { getSupportedJwaSignatureAlgorithms } from '../../shared/utils' +import { OpenId4VciCredentialFormatProfile } from '../shared' +import { + getTypesFromCredentialSupported, + handleAuthorizationDetails, + getOfferedCredentials, +} from '../shared/issuerMetadataUtils' +import { getSupportedJwaSignatureAlgorithms } from '../shared/utils' import { type AuthCodeFlowOptions, @@ -66,36 +67,11 @@ import { type SupportedCredentialFormats, supportedCredentialFormats, } from './OpenId4VciHolderServiceOptions' -import { OpenId4VciCredentialFormatProfile } from './utils' -import { getFormatForVersion, getUniformFormat } from './utils/Formats' -import { - getMetadataFromCredentialOffer, - getOfferedCredentialsWithMetadata, - getSupportedCredentials, - handleAuthorizationDetails, - OfferedCredentialType, -} from './utils/IssuerMetadataUtils' - -// FIXME: remove support for draft 8 -function getV8CredentialType(offeredCredentialWithMetadata: OfferedCredentialWithMetadata, version: OpenId4VCIVersion) { - if (offeredCredentialWithMetadata.offerType === OfferedCredentialType.InlineCredentialOffer) { - throw new AriesFrameworkError(`Inline credential offers not supported for version < 11`) - } - - if (!offeredCredentialWithMetadata.credentialSupported.id) { - throw new AriesFrameworkError( // This should not happen - `No id provided for a credential supported entry in combination with the OpenId4VCI v8 draft` - ) - } - - const originalFormat = getFormatForVersion(offeredCredentialWithMetadata.format, version) - const credentialType = offeredCredentialWithMetadata.credentialSupported.id.split(`-${originalFormat}`)[0] - return credentialType -} +// FIXME: this is also defined in the sphereon lib, is there a reason we don't use that one? async function createAuthorizationRequestUri(options: { - credentialOffer: CredentialOfferPayloadV1_0_11 - metadata: EndpointMetadataResult + credentialOffer: OpenId4VciCredentialOfferPayload + metadata: ResolvedCredentialOffer['metadata'] clientId: string codeChallenge: string codeChallengeMethod: CodeChallengeMethod @@ -115,7 +91,7 @@ async function createAuthorizationRequestUri(options: { // Authorization servers supporting PAR SHOULD include the URL of their pushed authorization request endpoint in their authorization server metadata document // Note that the presence of pushed_authorization_request_endpoint is sufficient for a client to determine that it may use the PAR flow. - const parEndpoint = metadata.credentialIssuerMetadata?.pushed_authorization_request_endpoint + const parEndpoint = metadata.credentialIssuerMetadata.pushed_authorization_request_endpoint const authorizationEndpoint = metadata.credentialIssuerMetadata?.authorization_endpoint @@ -185,25 +161,23 @@ export class OpenId4VciHolderService { this.logger = logger } - public async resolveCredentialOffer( - credentialOffer: UniformCredentialOfferPayload | string, - opts?: { version?: OpenId4VCIVersion } - ): Promise { - let version = opts?.version ?? OpenId4VCIVersion.VER_1_0_11 + public async resolveCredentialOffer(credentialOffer: string): Promise { + const client = await OpenID4VCIClient.fromURI({ + uri: credentialOffer, + resolveOfferUri: true, + retrieveServerMetadata: true, + }) - if (typeof credentialOffer === 'string' && URL.canParse(credentialOffer)) { - const credentialOfferWithBaseUrl = await CredentialOfferClient.fromURI(credentialOffer) - credentialOffer = credentialOfferWithBaseUrl.credential_offer - version = credentialOfferWithBaseUrl.version + if (!client.credentialOffer?.credential_offer) { + throw new AriesFrameworkError(`Could not resolve credential offer from '${credentialOffer}'`) } + const credentialOfferPayload: OpenId4VciCredentialOfferPayload = client.credentialOffer?.credential_offer - const uniformCredentialOffer = { - credential_offer: typeof credentialOffer === 'string' ? undefined : credentialOffer, - credential_offer_uri: typeof credentialOffer === 'string' ? credentialOffer : undefined, + const metadata = await client.retrieveServerMetadata() + if (!metadata.credentialIssuerMetadata) { + throw new AriesFrameworkError(`Could not retrieve issuer metadata from '${metadata.issuer}'`) } - - const credentialOfferPayload = (await assertedUniformCredentialOffer(uniformCredentialOffer)).credential_offer - const { metadata, issuerMetadata } = await getMetadataFromCredentialOffer(credentialOfferPayload) + const issuerMetadata = metadata.credentialIssuerMetadata as OpenId4VciIssuerMetadata this.logger.info('Fetched server metadata', { issuer: metadata.issuer, @@ -213,50 +187,38 @@ export class OpenId4VciHolderService { this.logger.debug('Full server metadata', metadata) - const credentialsSupported = getSupportedCredentials({ issuerMetadata, version }) - const offeredCredentialsWithMetadata = getOfferedCredentialsWithMetadata( - credentialOfferPayload.credentials, - credentialsSupported - ) - return { - metadata, + metadata: { + ...metadata, + credentialIssuerMetadata: issuerMetadata, + }, credentialOfferPayload, - offeredCredentials: offeredCredentialsWithMetadata, - version, + offeredCredentials: getOfferedCredentials( + credentialOfferPayload.credentials, + issuerMetadata.credentials_supported + ), + version: client.version(), } } private getAuthDetailsFromOfferedCredential( - credentialWithMetadata: OfferedCredentialWithMetadata, - authDetailsLocation: string | undefined, - version: OpenId4VCIVersion + offeredCredential: OpenId4VciCredentialSupported, + authDetailsLocation: string | undefined ): AuthorizationDetails | undefined { - const { format, types, offerType } = credentialWithMetadata + const { format } = offeredCredential const type = 'openid_credential' - if (version < OpenId4VCIVersion.VER_1_0_11) { - // TODO: this is valid 08 - // const credentialType = getV8CredentialType(credentialWithMetadata, version) - // return { type, credential_type: credentialType, format } - return undefined - } - const locations = authDetailsLocation ? [authDetailsLocation] : undefined if (format === OpenId4VciCredentialFormatProfile.JwtVcJson) { - return { type, format, types, locations } + return { type, format, types: offeredCredential.types, locations } satisfies AuthorizationDetailsJwtVcJson } else if ( format === OpenId4VciCredentialFormatProfile.LdpVc || format === OpenId4VciCredentialFormatProfile.JwtVcJsonLd ) { - // Inline Credential Offers come with no context so we cannot create the authorization_details - // This type of credentials can only be requested via scopes - if (offerType === OfferedCredentialType.InlineCredentialOffer) return undefined - const credential_definition = { - '@context': credentialWithMetadata.credentialSupported['@context'], - credentialSubject: credentialWithMetadata.credentialSupported.credentialSubject, - types, + '@context': offeredCredential['@context'], + credentialSubject: offeredCredential.credentialSubject, + types: offeredCredential.types, } return { type, format, locations, credential_definition } @@ -265,50 +227,38 @@ export class OpenId4VciHolderService { type, format, locations, - vct: types[0], - claims: - offerType === OfferedCredentialType.InlineCredentialOffer - ? credentialWithMetadata.credentialOffer.claims - : credentialWithMetadata.credentialSupported.claims, + vct: offeredCredential.vct, + claims: offeredCredential.claims, } } else { throw new AriesFrameworkError(`Cannot create authorization_details. Unsupported credential format '${format}'.`) } } + // FIXME: this is an oid4vci authorization request + // while we also support siop/oid4vp authorization requests + // need to make sure difference is clear public async resolveAuthorizationRequest( agentContext: AgentContext, resolvedCredentialOffer: ResolvedCredentialOffer, authCodeFlowOptions: AuthCodeFlowOptions ): Promise { - const { credentialOfferPayload, metadata: _metadata, version } = resolvedCredentialOffer - const codeVerifier = ( - await Promise.all([agentContext.wallet.generateNonce(), agentContext.wallet.generateNonce()]) - ).join('') + const { credentialOfferPayload, metadata, offeredCredentials } = resolvedCredentialOffer + const codeVerifier = `${await agentContext.wallet.generateNonce()}${await agentContext.wallet.generateNonce()}` const codeVerifierSha256 = Hasher.hash(TypedArrayEncoder.fromString(codeVerifier), 'sha2-256') const codeChallenge = TypedArrayEncoder.toBase64URL(codeVerifierSha256) - const { metadata, issuerMetadata } = await getMetadataFromCredentialOffer(credentialOfferPayload, _metadata) - const credentialsSupported = getSupportedCredentials({ issuerMetadata, version }) - - const offeredCredentialsWithMetadata = getOfferedCredentialsWithMetadata( - credentialOfferPayload.credentials, - credentialsSupported - ) - this.logger.debug('Converted code_verifier to code_challenge', { codeVerifier: codeVerifier, sha256: codeVerifierSha256.toString(), base64Url: codeChallenge, }) - let authDetailsLocation: string | undefined - if (issuerMetadata.authorization_server) { - authDetailsLocation = metadata.issuer - } - - const authDetails = offeredCredentialsWithMetadata - .map((credential) => this.getAuthDetailsFromOfferedCredential(credential, authDetailsLocation, version)) + const authDetailsLocation = metadata.credentialIssuerMetadata.authorization_server + ? metadata.credentialIssuerMetadata.authorization_server + : undefined + const authDetails = offeredCredentials + .map((credential) => this.getAuthDetailsFromOfferedCredential(credential, authDetailsLocation)) .filter((authDetail): authDetail is AuthorizationDetails => authDetail !== undefined) const { clientId, redirectUri, scope } = authCodeFlowOptions @@ -320,7 +270,7 @@ export class OpenId4VciHolderService { codeChallengeMethod: CodeChallengeMethod.SHA256, // TODO: Read HAIP SdJwtVc's should always be requested via scopes // TODO: should we now always use scopes instead of authDetails? or both???? - scope: [...(scope ?? [])], + scope: scope ?? [], authDetails, metadata, }) @@ -341,7 +291,7 @@ export class OpenId4VciHolderService { } ) { const { resolvedCredentialOffer, acceptCredentialOfferOptions, resolvedAuthorizationRequestWithCode } = options - const { credentialOfferPayload, metadata: _metadata, version } = resolvedCredentialOffer + const { credentialOfferPayload, metadata, version, offeredCredentials } = resolvedCredentialOffer const { credentialsToRequest, userPin, credentialBindingResolver, verifyCredentialStatus } = acceptCredentialOfferOptions @@ -353,7 +303,6 @@ export class OpenId4VciHolderService { this.logger.info(`Accepting the following credential offers '${credentialsToRequest}'`) - const { metadata, issuerMetadata } = await getMetadataFromCredentialOffer(credentialOfferPayload, _metadata) const supportedJwaSignatureAlgorithms = getSupportedJwaSignatureAlgorithms(agentContext) const allowedProofOfPossessionSigAlgs = acceptCredentialOfferOptions.allowedProofOfPossessionSignatureAlgorithms @@ -378,7 +327,7 @@ export class OpenId4VciHolderService { if (resolvedAuthorizationRequestWithCode) { const { code, codeVerifier, redirectUri } = resolvedAuthorizationRequestWithCode accessTokenResponse = await accessTokenClient.acquireAccessToken({ - metadata, + metadata: metadata, credentialOffer: { credential_offer: credentialOfferPayload }, pin: userPin, code, @@ -387,7 +336,7 @@ export class OpenId4VciHolderService { }) } else { accessTokenResponse = await accessTokenClient.acquireAccessToken({ - metadata, + metadata: metadata, credentialOffer: { credential_offer: credentialOfferPayload }, pin: userPin, }) @@ -400,40 +349,14 @@ export class OpenId4VciHolderService { this.logger.debug('Requested OpenId4VCI Access Token.') const accessToken = accessTokenResponse.successBody - - const credentialsSupported = getSupportedCredentials({ issuerMetadata, version }) - - const offeredCredentialsWithMetadata = getOfferedCredentialsWithMetadata( - credentialOfferPayload.credentials, - credentialsSupported - ) - - const credentialsToRequestWithMetadata = credentialsToRequest?.map((ctr) => { - const credentialToRequest = offeredCredentialsWithMetadata.find((offeredCredentialWithMetadata) => { - const { format, types } = offeredCredentialWithMetadata - return ctr.format === format && equalsIgnoreOrder(ctr.types, types) - }) - - if (!credentialToRequest) - throw new AriesFrameworkError( - [ - `Could not find the the requested credential with format '${ctr.format}'`, - `and types '${ctr.types.join()}' in the offered credentials.`, - ].join(' ') - ) - - return credentialToRequest - }) - - const receivedCredentials: (W3cVerifiableCredential | SdJwtVc)[] = [] - + const receivedCredentials: Array = [] let newCNonce: string | undefined - for (const credentialWithMetadata of credentialsToRequestWithMetadata ?? offeredCredentialsWithMetadata) { + for (const offeredCredential of credentialsToRequest ?? offeredCredentials) { // Get all options for the credential request (such as which kid to use, the signature algorithm, etc) const { credentialBinding, signatureAlgorithm } = await this.getCredentialRequestOptions(agentContext, { possibleProofOfPossessionSignatureAlgorithms: possibleProofOfPossessionSigAlgs, - offeredCredentialWithMetadata: credentialWithMetadata, + offeredCredential, credentialBindingResolver, }) @@ -464,23 +387,11 @@ export class OpenId4VciHolderService { .withCredentialEndpoint(metadata.credential_endpoint) .withTokenFromResponse(accessToken) - const format = credentialWithMetadata.format - - let credentialTypes: string | string[] - if (version < OpenId4VCIVersion.VER_1_0_11) { - if (credentialWithMetadata.offerType === OfferedCredentialType.InlineCredentialOffer) { - throw new AriesFrameworkError(`Inline credential offers not supported for version < 11`) - } - credentialTypes = getV8CredentialType(credentialWithMetadata, version) - } else { - credentialTypes = credentialWithMetadata.types - } - const credentialRequestClient = credentialRequestBuilder.build() const credentialResponse = await credentialRequestClient.acquireCredentialsUsingProof({ proofInput: proofOfPossession, - credentialTypes, - format: getFormatForVersion(format, version), + credentialTypes: getTypesFromCredentialSupported(offeredCredential), + format: offeredCredential.format, }) newCNonce = credentialResponse.successBody?.c_nonce @@ -508,12 +419,12 @@ export class OpenId4VciHolderService { options: { credentialBindingResolver: CredentialBindingResolver possibleProofOfPossessionSignatureAlgorithms: JwaSignatureAlgorithm[] - offeredCredentialWithMetadata: OfferedCredentialWithMetadata + offeredCredential: OpenId4VciCredentialSupportedWithId } ) { const { signatureAlgorithm, supportedDidMethods, supportsAllDidMethods, supportsJwk } = this.getProofOfPossessionRequirements(agentContext, { - credentialsToRequest: options.offeredCredentialWithMetadata, + credentialToRequest: options.offeredCredential, possibleProofOfPossessionSignatureAlgorithms: options.possibleProofOfPossessionSignatureAlgorithms, }) @@ -526,7 +437,7 @@ export class OpenId4VciHolderService { const supportedVerificationMethods = getSupportedVerificationMethodTypesFromKeyType(JwkClass.keyType) - const format = options.offeredCredentialWithMetadata.format as SupportedCredentialFormats + const format = options.offeredCredential.format as SupportedCredentialFormats // Now we need to determine how the credential will be bound to us const credentialBinding = await options.credentialBindingResolver({ @@ -534,10 +445,7 @@ export class OpenId4VciHolderService { signatureAlgorithm, supportedVerificationMethods, keyType: JwkClass.keyType, - supportedCredentialId: - options.offeredCredentialWithMetadata.offerType === OfferedCredentialType.CredentialSupported - ? options.offeredCredentialWithMetadata.credentialSupported.id - : undefined, + supportedCredentialId: options.offeredCredential.id, supportsAllDidMethods, supportedDidMethods, supportsJwk, @@ -584,22 +492,20 @@ export class OpenId4VciHolderService { private getProofOfPossessionRequirements( agentContext: AgentContext, options: { - credentialsToRequest: OfferedCredentialWithMetadata + credentialToRequest: OpenId4VciCredentialSupportedWithId possibleProofOfPossessionSignatureAlgorithms: JwaSignatureAlgorithm[] } ): ProofOfPossessionRequirements { - const { credentialsToRequest } = options + const { credentialToRequest } = options - if (credentialsToRequest.offerType === OfferedCredentialType.CredentialSupported) { - if (!supportedCredentialFormats.includes(credentialsToRequest.format as SupportedCredentialFormats)) { - throw new AriesFrameworkError( - [ - `Requested credential with format '${credentialsToRequest.format}',`, - `for the credential of type '${credentialsToRequest.types.join(', ')},`, - `but the wallet only supports the following formats '${supportedCredentialFormats.join(', ')}'`, - ].join('\n') - ) - } + if (!supportedCredentialFormats.includes(credentialToRequest.format as SupportedCredentialFormats)) { + throw new AriesFrameworkError( + [ + `Requested credential with format '${credentialToRequest.format}',`, + `for the credential with id '${credentialToRequest.id},`, + `but the wallet only supports the following formats '${supportedCredentialFormats.join(', ')}'`, + ].join('\n') + ) } // For each of the supported algs, find the key types, then find the proof types @@ -607,20 +513,15 @@ export class OpenId4VciHolderService { let signatureAlgorithm: JwaSignatureAlgorithm | undefined - const credentialSupported = - credentialsToRequest.offerType === OfferedCredentialType.CredentialSupported - ? credentialsToRequest.credentialSupported - : undefined - - const issuerSupportedCryptographicSuites = credentialSupported?.cryptographic_suites_supported - const issuerSupportedBindingMethods = credentialSupported?.cryptographic_binding_methods_supported + const issuerSupportedCryptographicSuites = credentialToRequest.cryptographic_suites_supported + const issuerSupportedBindingMethods = credentialToRequest.cryptographic_binding_methods_supported // If undefined, it means the issuer didn't include the cryptographic suites in the metadata // We just guess that the first one is supported if (issuerSupportedCryptographicSuites === undefined) { signatureAlgorithm = options.possibleProofOfPossessionSignatureAlgorithms[0] } else { - switch (credentialsToRequest.format) { + switch (credentialToRequest.format) { case OpenId4VciCredentialFormatProfile.JwtVcJson: case OpenId4VciCredentialFormatProfile.JwtVcJsonLd: case OpenId4VciCredentialFormatProfile.SdJwtVc: @@ -646,9 +547,7 @@ export class OpenId4VciHolderService { if (!signatureAlgorithm) { throw new AriesFrameworkError( - `Could not establish signature algorithm for format ${credentialsToRequest.format} and id ${ - credentialSupported?.id ?? 'Inline credential offer' - }` + `Could not establish signature algorithm for format ${credentialToRequest.format} and id ${credentialToRequest.id}` ) } @@ -676,7 +575,7 @@ export class OpenId4VciHolderService { throw new AriesFrameworkError('Did not receive a successful credential response.') } - const format = getUniformFormat(credentialResponse.successBody.format) + const format = credentialResponse.successBody.format if (format === OpenId4VciCredentialFormatProfile.SdJwtVc) { if (typeof credentialResponse.successBody.credential !== 'string') throw new AriesFrameworkError( diff --git a/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderServiceOptions.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderServiceOptions.ts similarity index 90% rename from packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderServiceOptions.ts rename to packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderServiceOptions.ts index b448d60964..0369a6260e 100644 --- a/packages/openid4vc/src/openid4vc-holder/reception/OpenId4VciHolderServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderServiceOptions.ts @@ -1,14 +1,13 @@ -import type { OfferedCredentialWithMetadata } from './utils/IssuerMetadataUtils' -import type { CredentialHolderBinding } from '../../shared' -import type { JwaSignatureAlgorithm, KeyType } from '@aries-framework/core' import type { - CredentialOfferPayloadV1_0_11, - EndpointMetadataResult, - OpenId4VCIVersion, - AuthorizationDetails, -} from '@sphereon/oid4vci-common' + CredentialHolderBinding, + OpenId4VciCredentialOfferPayload, + OpenId4VciCredentialSupportedWithId, + OpenId4VciIssuerMetadata, +} from '../shared' +import type { JwaSignatureAlgorithm, KeyType } from '@aries-framework/core' +import type { AuthorizationServerMetadata, EndpointMetadataResult, OpenId4VCIVersion } from '@sphereon/oid4vci-common' -import { OpenId4VciCredentialFormatProfile } from './utils/claimFormatMapping' +import { OpenId4VciCredentialFormatProfile } from '../shared/models/OpenId4VciCredentialFormatProfile' export type SupportedCredentialFormats = | OpenId4VciCredentialFormatProfile.JwtVcJson @@ -23,13 +22,13 @@ export const supportedCredentialFormats: SupportedCredentialFormats[] = [ OpenId4VciCredentialFormatProfile.LdpVc, ] -export type { OpenId4VCIVersion, EndpointMetadataResult, CredentialOfferPayloadV1_0_11, AuthorizationDetails } - export interface ResolvedCredentialOffer { - metadata: EndpointMetadataResult - credentialOfferPayload: CredentialOfferPayloadV1_0_11 + metadata: EndpointMetadataResult & { + credentialIssuerMetadata: Partial & OpenId4VciIssuerMetadata + } + credentialOfferPayload: OpenId4VciCredentialOfferPayload + offeredCredentials: OpenId4VciCredentialSupportedWithId[] version: OpenId4VCIVersion - offeredCredentials: OfferedCredentialWithMetadata[] } export interface ResolvedAuthorizationRequest extends AuthCodeFlowOptions { @@ -54,8 +53,9 @@ export interface AcceptCredentialOfferOptions { /** * This is the list of credentials that will be requested from the issuer. * If not provided all offered credentials will be requested. + * FIXME: should this be an list of ids? */ - credentialsToRequest?: OfferedCredentialWithMetadata[] + credentialsToRequest?: OpenId4VciCredentialSupportedWithId[] verifyCredentialStatus?: boolean diff --git a/packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderService.ts similarity index 94% rename from packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderService.ts rename to packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderService.ts index 9db95de7fd..de4d42d333 100644 --- a/packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderService.ts @@ -12,9 +12,6 @@ import { injectable, W3cJsonLdVerifiablePresentation, asArray, - inject, - InjectionSymbols, - Logger, parseDid, DifPresentationExchangeService, } from '@aries-framework/core' @@ -40,14 +37,7 @@ import { @injectable() export class OpenId4VpHolderService { - private logger: Logger - - public constructor( - @inject(InjectionSymbols.Logger) logger: Logger, - private presentationExchangeService: DifPresentationExchangeService - ) { - this.logger = logger - } + public constructor(private presentationExchangeService: DifPresentationExchangeService) {} private async getOpenIdProvider( agentContext: AgentContext, @@ -64,9 +54,6 @@ export class OpenId4VpHolderService { .withSupportedVersions([SupportedVersion.SIOPv2_D11, SupportedVersion.SIOPv2_D12_OID4VP_D18]) .withCustomResolver(getResolver(agentContext)) .withCheckLinkedDomain(CheckLinkedDomain.NEVER) - // .withPresentationSignCallback - // .withEventEmitter - // .withRegistration() if (verificationMethod) { const { signature, did, kid, alg } = await getSuppliedSignatureFromVerificationMethod( @@ -99,8 +86,10 @@ export class OpenId4VpHolderService { }, }) - this.logger.debug(`verified SIOP Authorization Request for issuer '${verifiedAuthorizationRequest.issuer}'`) - this.logger.debug(`requestJwtOrUri '${requestJwtOrUri}'`) + agentContext.config.logger.debug( + `verified SIOP Authorization Request for issuer '${verifiedAuthorizationRequest.issuer}'` + ) + agentContext.config.logger.debug(`requestJwtOrUri '${requestJwtOrUri}'`) // If the presentationDefinitions array property is present it means the op.verifyAuthorizationRequest // already has established that the Presentation Definition(s) itself were valid and present. diff --git a/packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderServiceOptions.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderServiceOptions.ts similarity index 100% rename from packages/openid4vc/src/openid4vc-holder/presentation/OpenId4VpHolderServiceOptions.ts rename to packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderServiceOptions.ts diff --git a/packages/openid4vc/src/openid4vc-holder/presentation/index.ts b/packages/openid4vc/src/openid4vc-holder/presentation/index.ts deleted file mode 100644 index ec4b86d2dc..0000000000 --- a/packages/openid4vc/src/openid4vc-holder/presentation/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './OpenId4VpHolderService' -export * from './OpenId4VpHolderServiceOptions' diff --git a/packages/openid4vc/src/openid4vc-holder/reception/index.ts b/packages/openid4vc/src/openid4vc-holder/reception/index.ts deleted file mode 100644 index 125586e082..0000000000 --- a/packages/openid4vc/src/openid4vc-holder/reception/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './OpenId4VciHolderService' -export * from './OpenId4VciHolderServiceOptions' -export * from './utils' diff --git a/packages/openid4vc/src/openid4vc-holder/reception/utils/Formats.ts b/packages/openid4vc/src/openid4vc-holder/reception/utils/Formats.ts deleted file mode 100644 index 660b7df4fa..0000000000 --- a/packages/openid4vc/src/openid4vc-holder/reception/utils/Formats.ts +++ /dev/null @@ -1,45 +0,0 @@ -import type { CredentialFormat } from '@sphereon/ssi-types' - -import { AriesFrameworkError } from '@aries-framework/core' -import { OpenId4VCIVersion } from '@sphereon/oid4vci-common' - -import { OpenId4VciCredentialFormatProfile } from './claimFormatMapping' - -// Based on https://github.com/Sphereon-Opensource/OID4VCI/pull/54/files - -// check if a string is a valid enum value of OpenIdCredentialFormatProfile - -const isUniformFormat = (format: string): format is OpenId4VciCredentialFormatProfile => { - return Object.values(OpenId4VciCredentialFormatProfile).includes(format as OpenId4VciCredentialFormatProfile) -} - -export function getUniformFormat( - format: string | OpenId4VciCredentialFormatProfile | CredentialFormat -): OpenId4VciCredentialFormatProfile { - // Already valid format - if (isUniformFormat(format)) return format - - // Older formats - if (format === 'jwt_vc' || format === 'jwt') { - return OpenId4VciCredentialFormatProfile.JwtVcJson - } - if (format === 'ldp_vc' || format === 'ldp') { - return OpenId4VciCredentialFormatProfile.LdpVc - } - - throw new AriesFrameworkError(`Invalid format: ${format}`) -} - -export function getFormatForVersion(format: string, version: OpenId4VCIVersion) { - const uniformFormat = getUniformFormat(format) - - if (version < OpenId4VCIVersion.VER_1_0_11) { - if (uniformFormat === 'jwt_vc_json') { - return 'jwt_vc' as const - } else if (uniformFormat === 'ldp_vc' || uniformFormat === 'jwt_vc_json-ld') { - return 'ldp_vc' as const - } - } - - return uniformFormat -} diff --git a/packages/openid4vc/src/openid4vc-holder/reception/utils/IssuerMetadataUtils.ts b/packages/openid4vc/src/openid4vc-holder/reception/utils/IssuerMetadataUtils.ts deleted file mode 100644 index f6e08e1e31..0000000000 --- a/packages/openid4vc/src/openid4vc-holder/reception/utils/IssuerMetadataUtils.ts +++ /dev/null @@ -1,276 +0,0 @@ -import type { - AuthorizationDetails, - CredentialIssuerMetadata, - CredentialOfferFormat, - CredentialOfferFormatJwtVcJson, - CredentialOfferFormatJwtVcJsonLdAndLdpVc, - CredentialOfferFormatSdJwtVc, - CredentialOfferPayloadV1_0_11, - CredentialSupported, - CredentialSupportedJwtVcJson, - CredentialSupportedJwtVcJsonLdAndLdpVc, - CredentialSupportedSdJwtVc, - CredentialSupportedTypeV1_0_08, - CredentialSupportedV1_0_08, - EndpointMetadataResult, - IssuerMetadataV1_0_08, -} from '@sphereon/oid4vci-common' - -import { AriesFrameworkError } from '@aries-framework/core' -import { MetadataClient } from '@sphereon/oid4vci-client' -import { OpenId4VCIVersion } from '@sphereon/oid4vci-common' - -import { getUniformFormat, getFormatForVersion } from './Formats' -import { OpenId4VciCredentialFormatProfile } from './claimFormatMapping' - -/** - * The type of a credential offer entry. For each item in `credentials` array, the type MUST be one of the following: - * - CredentialSupported, when the value is a string and points to a credential from the `credentials_supported` array. - * - InlineCredentialOffer, when the value is a JSON object that represents an inline credential offer. - */ -export enum OfferedCredentialType { - CredentialSupported = 'CredentialSupported', - InlineCredentialOffer = 'InlineCredentialOffer', -} - -export type InlineOfferedCredentialWithMetadata = - | { - offerType: OfferedCredentialType.InlineCredentialOffer - format: OpenId4VciCredentialFormatProfile.JwtVcJson - credentialOffer: CredentialOfferFormatJwtVcJson - types: string[] - } - | { - offerType: OfferedCredentialType.InlineCredentialOffer - format: OpenId4VciCredentialFormatProfile.JwtVcJsonLd | OpenId4VciCredentialFormatProfile.LdpVc - credentialOffer: CredentialOfferFormatJwtVcJsonLdAndLdpVc - types: string[] - } - | { - offerType: OfferedCredentialType.InlineCredentialOffer - format: OpenId4VciCredentialFormatProfile.SdJwtVc - credentialOffer: CredentialOfferFormatSdJwtVc - types: string[] - } - -export type ReferencedOfferedCredentialWithMetadata = - | { - offerType: OfferedCredentialType.CredentialSupported - format: OpenId4VciCredentialFormatProfile.JwtVcJson - credentialSupported: CredentialSupportedJwtVcJson - types: string[] - } - | { - offerType: OfferedCredentialType.CredentialSupported - format: OpenId4VciCredentialFormatProfile.JwtVcJsonLd | OpenId4VciCredentialFormatProfile.LdpVc - credentialSupported: CredentialSupportedJwtVcJsonLdAndLdpVc - types: string[] - } - | { - offerType: OfferedCredentialType.CredentialSupported - format: OpenId4VciCredentialFormatProfile.SdJwtVc - credentialSupported: CredentialSupportedSdJwtVc - types: string[] - } - -export type OfferedCredentialWithMetadata = - | ReferencedOfferedCredentialWithMetadata - | InlineOfferedCredentialWithMetadata - -/** - * Returns all entries from the credential offer with the associated metadata resolved. For inline entries, the offered credential object - * is included directly. For 'id' entries, the associated `credentials_supported` object is resolved from the issuer metadata. - * - * NOTE: for v1_0-08, a single credential id in the issuer metadata could have multiple formats. This means that the returned value - * from this method could contain multiple entries for a single credential id, but with different formats. This is detectable as the - * id will be the `-`. - */ -export function getOfferedCredentialsWithMetadata( - credentialOffers: (CredentialOfferFormat | string)[], - supportedCredentials: CredentialSupported[] -) { - const offeredCredentialsWithMetadata: OfferedCredentialWithMetadata[] = [] - - for (const offeredCredential of credentialOffers) { - // If the offeredCredential is a string, it has to reference a supported credential in the issuer metadata - if (typeof offeredCredential === 'string') { - const foundSupportedCredentials = supportedCredentials.filter( - (supportedCredential) => - supportedCredential.id === offeredCredential || - supportedCredential.id === - `${offeredCredential}-${getFormatForVersion(supportedCredential.format, OpenId4VCIVersion.VER_1_0_08)}` - ) - - // Make sure the issuer metadata includes the offered credential. - if (foundSupportedCredentials.length === 0) { - throw new Error( - `Offered credential '${offeredCredential}' is not part of credentials_supported of the issuer metadata.` - ) - } - - for (const foundSupportedCredential of foundSupportedCredentials) { - if (foundSupportedCredential.format === 'vc+sd-jwt') { - offeredCredentialsWithMetadata.push({ - offerType: OfferedCredentialType.CredentialSupported, - credentialSupported: foundSupportedCredential, - format: OpenId4VciCredentialFormatProfile.SdJwtVc, - types: [foundSupportedCredential.vct], - }) - } else { - offeredCredentialsWithMetadata.push({ - offerType: OfferedCredentialType.CredentialSupported, - credentialSupported: foundSupportedCredential, - format: getUniformFormat(foundSupportedCredential.format), - types: foundSupportedCredential.types, - } as OfferedCredentialWithMetadata) - } - } - } - // Otherwise it's an inline credential offer that does not reference a supported credential in the issuer metadata - else { - let types: string[] - if (offeredCredential.format === 'jwt_vc_json') { - types = offeredCredential.types - } else if (offeredCredential.format === 'jwt_vc_json-ld' || offeredCredential.format === 'ldp_vc') { - types = offeredCredential.credential_definition.types - } else if (offeredCredential.format === 'vc+sd-jwt') { - types = [offeredCredential.vct] - } else { - throw new AriesFrameworkError(`Unknown format received ${JSON.stringify(offeredCredential.format)}`) - } - - offeredCredentialsWithMetadata.push({ - offerType: OfferedCredentialType.InlineCredentialOffer, - format: getUniformFormat(offeredCredential.format), - types: types, - credentialOffer: offeredCredential, - } as OfferedCredentialWithMetadata) - } - } - - return offeredCredentialsWithMetadata -} - -export async function getMetadataFromCredentialOffer( - credentialOfferPayload: CredentialOfferPayloadV1_0_11, - metadata?: EndpointMetadataResult -) { - const issuer = credentialOfferPayload.credential_issuer - - const resolvedMetadata = metadata?.credentialIssuerMetadata - ? metadata - : await MetadataClient.retrieveAllMetadata(issuer) - - if (!resolvedMetadata) { - throw new AriesFrameworkError(`Could not retrieve metadata for OpenId4Vci issuer: ${issuer}`) - } - - const issuerMetadata = resolvedMetadata.credentialIssuerMetadata - if (!issuerMetadata) { - throw new AriesFrameworkError(`Could not retrieve issuer metadata for OpenId4Vci issuer: ${issuer}`) - } - - return { issuer, metadata: resolvedMetadata, issuerMetadata } -} - -export function getSupportedCredentials(opts: { - issuerMetadata: CredentialIssuerMetadata | IssuerMetadataV1_0_08 - version: OpenId4VCIVersion -}): CredentialSupported[] { - const { issuerMetadata } = opts - let credentialsSupported: CredentialSupported[] - const { version } = opts ?? { version: OpenId4VCIVersion.VER_1_0_11 } - - const usesTransformedCredentialsSupported = - version === OpenId4VCIVersion.VER_1_0_08 || !Array.isArray(issuerMetadata.credentials_supported) - if (usesTransformedCredentialsSupported) { - credentialsSupported = credentialsSupportedV8ToV11((issuerMetadata as IssuerMetadataV1_0_08).credentials_supported) - } else { - credentialsSupported = (issuerMetadata as CredentialIssuerMetadata).credentials_supported - } - - if (credentialsSupported === undefined || credentialsSupported.length === 0) { - return [] - } else { - return credentialsSupported - } -} - -export function credentialsSupportedV8ToV11(supportedV8: CredentialSupportedTypeV1_0_08): CredentialSupported[] { - return Object.entries(supportedV8).flatMap((entry) => { - const credentialId = entry[0] - const supportedV8 = entry[1] - return credentialSupportedV8ToV11(credentialId, supportedV8) - }) -} - -export function credentialSupportedV8ToV11( - credentialId: string, - supportedV8: CredentialSupportedV1_0_08 -): CredentialSupported[] { - const v8FormatEntries = Object.entries(supportedV8.formats) - - return v8FormatEntries.map((entry) => { - const format = entry[0] - const credentialSupportedV8 = entry[1] - if (typeof format !== 'string') { - throw Error(`Unknown format received ${JSON.stringify(format)}`) - } - - // v8 format included the credential type / id as the key of the object and it could contain multiple supported formats - // v11 format has an array where each entry only supports one format, and can only have an `id` property. We include the - // key from the v8 object as the id for the v11 object, but to prevent collisions (as multiple formats can be supported under - // one key), we append the format to the key IF there's more than one format supported under the key. - const id = v8FormatEntries.length > 1 ? `${credentialId}-${format}` : credentialId - - let credentialSupported: CredentialSupported - const v11Format = getUniformFormat(format) - if (v11Format === OpenId4VciCredentialFormatProfile.JwtVcJson) { - credentialSupported = { - format: OpenId4VciCredentialFormatProfile.JwtVcJson, - display: supportedV8.display, - ...credentialSupportedV8, - credentialSubject: supportedV8.claims, - id, - } - } else if ( - v11Format === OpenId4VciCredentialFormatProfile.JwtVcJsonLd || - v11Format === OpenId4VciCredentialFormatProfile.LdpVc - ) { - credentialSupported = { - format: v11Format, - display: supportedV8.display, - ...credentialSupportedV8, - id, - '@context': ['VerifiableCredential'], // NOTE: V8 credentials don't come with @context - } - } else { - throw new AriesFrameworkError(`Invalid format received for OpenId4Vci V8 '${format}'`) - } - - return credentialSupported - }) -} - -// copied from sphereon -export function handleAuthorizationDetails( - authorizationDetails: AuthorizationDetails | AuthorizationDetails[], - metadata: EndpointMetadataResult -): AuthorizationDetails | AuthorizationDetails[] | undefined { - if (Array.isArray(authorizationDetails)) { - return authorizationDetails.map((value) => handleLocations(value, metadata)) - } else { - return handleLocations(authorizationDetails, metadata) - } -} - -// copied from sphereon -export function handleLocations(authorizationDetails: AuthorizationDetails, metadata: EndpointMetadataResult) { - if (typeof authorizationDetails === 'string') return authorizationDetails - if (metadata.credentialIssuerMetadata?.authorization_server || metadata.authorization_endpoint) { - if (!authorizationDetails.locations) authorizationDetails.locations = [metadata.issuer] - else if (Array.isArray(authorizationDetails.locations)) authorizationDetails.locations.push(metadata.issuer) - else authorizationDetails.locations = [authorizationDetails.locations as string, metadata.issuer] - } - return authorizationDetails -} diff --git a/packages/openid4vc/src/openid4vc-holder/reception/utils/__tests__/claimFormatMapping.test.ts b/packages/openid4vc/src/openid4vc-holder/reception/utils/__tests__/claimFormatMapping.test.ts deleted file mode 100644 index cf01f81ada..0000000000 --- a/packages/openid4vc/src/openid4vc-holder/reception/utils/__tests__/claimFormatMapping.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { AriesFrameworkError, ClaimFormat } from '@aries-framework/core' - -import { - fromDifClaimFormatToOpenIdCredentialFormatProfile, - fromOpenIdCredentialFormatProfileToDifClaimFormat, - OpenId4VciCredentialFormatProfile, -} from '../claimFormatMapping' - -describe('claimFormatMapping', () => { - it('should convert from openid credential format profile to DIF claim format', () => { - expect(fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.LdpVc)).toStrictEqual( - OpenId4VciCredentialFormatProfile.LdpVc - ) - - expect(fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.JwtVc)).toStrictEqual( - OpenId4VciCredentialFormatProfile.JwtVcJson - ) - - expect(() => fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.Jwt)).toThrow(AriesFrameworkError) - - expect(() => fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.Ldp)).toThrow(AriesFrameworkError) - - expect(() => fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.JwtVp)).toThrow(AriesFrameworkError) - - expect(() => fromDifClaimFormatToOpenIdCredentialFormatProfile(ClaimFormat.LdpVp)).toThrow(AriesFrameworkError) - }) - - it('should convert from DIF claim format to openid credential format profile', () => { - expect( - fromOpenIdCredentialFormatProfileToDifClaimFormat(OpenId4VciCredentialFormatProfile.JwtVcJson) - ).toStrictEqual(ClaimFormat.JwtVc) - - expect( - fromOpenIdCredentialFormatProfileToDifClaimFormat(OpenId4VciCredentialFormatProfile.JwtVcJsonLd) - ).toStrictEqual(ClaimFormat.JwtVc) - - expect(fromOpenIdCredentialFormatProfileToDifClaimFormat(OpenId4VciCredentialFormatProfile.LdpVc)).toStrictEqual( - ClaimFormat.LdpVc - ) - }) -}) diff --git a/packages/openid4vc/src/openid4vc-holder/reception/utils/claimFormatMapping.ts b/packages/openid4vc/src/openid4vc-holder/reception/utils/claimFormatMapping.ts deleted file mode 100644 index 0978af6cd5..0000000000 --- a/packages/openid4vc/src/openid4vc-holder/reception/utils/claimFormatMapping.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { AriesFrameworkError, ClaimFormat } from '@aries-framework/core' - -export enum OpenId4VciCredentialFormatProfile { - JwtVcJson = 'jwt_vc_json', - JwtVcJsonLd = 'jwt_vc_json-ld', - LdpVc = 'ldp_vc', - SdJwtVc = 'vc+sd-jwt', -} - -export const fromDifClaimFormatToOpenIdCredentialFormatProfile = ( - claimFormat: ClaimFormat -): OpenId4VciCredentialFormatProfile => { - switch (claimFormat) { - case ClaimFormat.JwtVc: - return OpenId4VciCredentialFormatProfile.JwtVcJson - case ClaimFormat.LdpVc: - return OpenId4VciCredentialFormatProfile.LdpVc - default: - throw new AriesFrameworkError( - `Unsupported DIF claim format, ${claimFormat}, to map to an openid credential format profile` - ) - } -} - -export const fromOpenIdCredentialFormatProfileToDifClaimFormat = ( - openidCredentialFormatProfile: OpenId4VciCredentialFormatProfile -): ClaimFormat => { - switch (openidCredentialFormatProfile) { - case OpenId4VciCredentialFormatProfile.JwtVcJson: - return ClaimFormat.JwtVc - case OpenId4VciCredentialFormatProfile.JwtVcJsonLd: - return ClaimFormat.JwtVc - case OpenId4VciCredentialFormatProfile.LdpVc: - return ClaimFormat.LdpVc - default: - throw new AriesFrameworkError( - `Unsupported openid credential format profile, ${openidCredentialFormatProfile}, to map to a DIF claim format` - ) - } -} diff --git a/packages/openid4vc/src/openid4vc-holder/reception/utils/index.ts b/packages/openid4vc/src/openid4vc-holder/reception/utils/index.ts deleted file mode 100644 index 5bee9d1118..0000000000 --- a/packages/openid4vc/src/openid4vc-holder/reception/utils/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './claimFormatMapping' -export * from './Formats' -export { OfferedCredentialType, OfferedCredentialWithMetadata } from './IssuerMetadataUtils' diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts index ad13a2fed4..81d9ba29b9 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts @@ -3,8 +3,8 @@ import type { OpenId4VciCreateCredentialOfferOptions, CredentialOffer, } from './OpenId4VcIssuerServiceOptions' -import type { OpenId4VcIssuerRecordProps } from './repository/OpenId4VcIssuerRecord' -import type { CredentialOfferPayloadV1_0_11 } from '@sphereon/oid4vci-common' +import type { OpenId4VcIssuerRecordProps } from './repository' +import type { OpenId4VciCredentialOfferPayload } from '../shared' import { injectable, AgentContext } from '@aries-framework/core' @@ -87,7 +87,7 @@ export class OpenId4VcIssuerApi { * @param uri - The URI referencing the credential offer. * @returns The credential offer payload associated with the given URI. */ - public async getCredentialOfferFromUri(uri: string): Promise { + public async getCredentialOfferFromUri(uri: string): Promise { return await this.openId4VcIssuerService.getCredentialOfferFromUri(this.agentContext, uri) } diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts index 037d004d66..b8f738a5a3 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts @@ -1,26 +1,24 @@ import type { + CreateCredentialResponseOptions, + CredentialOffer, OpenId4VciAuthorizationCodeFlowConfig, OpenId4VciCreateCredentialOfferOptions, - CreateCredentialResponseOptions, OpenId4VciCreateIssuerOptions, - CredentialOffer, - OpenId4VcIssuerMetadata, + OpenId4VciPreAuthorizedCodeFlowConfig, OpenId4VciSignCredential, OpenId4VciSignSdJwtCredential, OpenId4VciSignW3cCredential, - OpenId4VciPreAuthorizedCodeFlowConfig, + OpenId4VcIssuerMetadata, } from './OpenId4VcIssuerServiceOptions' -import type { ReferencedOfferedCredentialWithMetadata } from '../openid4vc-holder/reception/utils/IssuerMetadataUtils' -import type { CredentialHolderBinding } from '../shared' +import type { + CredentialHolderBinding, + OpenId4VciCredentialOfferPayload, + OpenId4VciCredentialRequest, + OpenId4VciCredentialSupported, +} from '../shared' import type { AgentContext, DidDocument } from '@aries-framework/core' import type { SdJwtVcModule } from '@aries-framework/sd-jwt-vc' -import type { - CredentialOfferPayloadV1_0_11, - CredentialRequestV1_0_11, - Grant, - JWTVerifyCallback, - CredentialSupported, -} from '@sphereon/oid4vci-common' +import type { Grant, JWTVerifyCallback } from '@sphereon/oid4vci-common' import type { CredentialDataSupplier, CredentialDataSupplierArgs, @@ -30,40 +28,35 @@ import type { import type { ICredential } from '@sphereon/ssi-types' import { - ClaimFormat, - JsonEncoder, - getJwkFromJson, - KeyType, - utils, AriesFrameworkError, + ClaimFormat, DidsApi, - JsonTransformer, - JwsService, - Jwt, - W3cCredential, - W3cCredentialService, equalsIgnoreOrder, getApiForModuleByName, + getJwkFromJson, getJwkFromKey, getKeyFromVerificationMethod, injectable, joinUriParts, + JsonEncoder, + JsonTransformer, + JwsService, + Jwt, + KeyType, + utils, + W3cCredential, + W3cCredentialService, } from '@aries-framework/core' import { IssueStatus } from '@sphereon/oid4vci-common' import { VcIssuerBuilder } from '@sphereon/oid4vci-issuer' -import { OpenId4VciCredentialFormatProfile } from '../openid4vc-holder' -import { - OfferedCredentialType, - getOfferedCredentialsWithMetadata, -} from '../openid4vc-holder/reception/utils/IssuerMetadataUtils' +import { getOfferedCredentials, OpenId4VciCredentialFormatProfile } from '../shared' import { storeActorIdForContextCorrelationId } from '../shared/router' import { getSphereonW3cVerifiableCredential } from '../shared/transform' import { getProofTypeFromKey } from '../shared/utils' import { OpenId4VcIssuerModuleConfig } from './OpenId4VcIssuerModuleConfig' -import { OpenId4VcIssuerRecord } from './repository/OpenId4VcIssuerRecord' -import { OpenId4VcIssuerRepository } from './repository/OpenId4VcIssuerRepository' +import { OpenId4VcIssuerRepository, OpenId4VcIssuerRecord } from './repository' const w3cOpenId4VcFormats = [ OpenId4VciCredentialFormatProfile.JwtVcJson, @@ -118,7 +111,7 @@ export class OpenId4VcIssuerService { // this checks if the structure of the credentials is correct // it throws an error if a offered credential cannot be found in the credentialsSupported - getOfferedCredentialsWithMetadata(offeredCredentials, vcIssuer.issuerMetadata.credentials_supported) + getOfferedCredentials(options.offeredCredentials, vcIssuer.issuerMetadata.credentials_supported) const { uri, session } = await vcIssuer.createCredentialOfferURI({ grants: await this.getGrantsFromConfig(agentContext, preAuthorizedCodeFlowConfig, authorizationCodeFlowConfig), @@ -332,32 +325,38 @@ export class OpenId4VcIssuerService { } private findOfferedCredentialsMatchingRequest( - credentialOffer: CredentialOfferPayloadV1_0_11, - credentialRequest: CredentialRequestV1_0_11, - credentialsSupported: CredentialSupported[] - ): ReferencedOfferedCredentialWithMetadata[] { - const offeredCredentials = getOfferedCredentialsWithMetadata(credentialOffer.credentials, credentialsSupported) - - // NOTE: we only support referenced offered credentials - // Filter out inline offers (should not be present in the first case as we don't support them at issuance) - const referencedOfferedCredentials = offeredCredentials.filter( - (offeredCredential): offeredCredential is ReferencedOfferedCredentialWithMetadata => - offeredCredential.offerType === OfferedCredentialType.CredentialSupported - ) + credentialOffer: OpenId4VciCredentialOfferPayload, + credentialRequest: OpenId4VciCredentialRequest, + credentialsSupported: OpenId4VciCredentialSupported[] + ): OpenId4VciCredentialSupported[] { + const offeredCredentials = getOfferedCredentials(credentialOffer.credentials, credentialsSupported) - return referencedOfferedCredentials.filter((offeredCredential) => { + return offeredCredentials.filter((offeredCredential) => { if (offeredCredential.format !== credentialRequest.format) return false - if (credentialRequest.format === OpenId4VciCredentialFormatProfile.JwtVcJson) { + if ( + credentialRequest.format === OpenId4VciCredentialFormatProfile.JwtVcJson && + offeredCredential.format === credentialRequest.format + ) { return equalsIgnoreOrder(offeredCredential.types, credentialRequest.types) } else if ( - credentialRequest.format === OpenId4VciCredentialFormatProfile.JwtVcJsonLd || - credentialRequest.format === OpenId4VciCredentialFormatProfile.LdpVc + credentialRequest.format === OpenId4VciCredentialFormatProfile.JwtVcJsonLd && + offeredCredential.format === credentialRequest.format ) { return equalsIgnoreOrder(offeredCredential.types, credentialRequest.credential_definition.types) - } else if (credentialRequest.format === OpenId4VciCredentialFormatProfile.SdJwtVc) { - return equalsIgnoreOrder(offeredCredential.types, [credentialRequest.vct]) + } else if ( + credentialRequest.format === OpenId4VciCredentialFormatProfile.LdpVc && + offeredCredential.format === credentialRequest.format + ) { + return equalsIgnoreOrder(offeredCredential.types, credentialRequest.credential_definition.types) + } else if ( + credentialRequest.format === OpenId4VciCredentialFormatProfile.SdJwtVc && + offeredCredential.format === credentialRequest.format + ) { + return offeredCredential.vct === credentialRequest.vct } + + return false }) } @@ -439,7 +438,7 @@ export class OpenId4VcIssuerService { } } - private async getHolderBindingFromRequest(credentialRequest: CredentialRequestV1_0_11) { + private async getHolderBindingFromRequest(credentialRequest: OpenId4VciCredentialRequest) { if (!credentialRequest.proof?.jwt) throw new AriesFrameworkError('Received a credential request without a proof') const jwt = Jwt.fromSerializedJwt(credentialRequest.proof.jwt) @@ -501,7 +500,7 @@ export class OpenId4VcIssuerService { credentialOffer, credentialRequest, - credentialsSupported: offeredCredentialsMatchingRequest.map((o) => o.credentialSupported), + credentialsSupported: offeredCredentialsMatchingRequest, }) } diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts index 351005befc..b60cc7c157 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts @@ -1,14 +1,14 @@ -import type { OpenId4VcIssuerRecordProps } from './repository/OpenId4VcIssuerRecord' +import type { OpenId4VcIssuerRecordProps } from './repository' import type { CredentialHolderBinding, OpenId4VciCredentialOffer, + OpenId4VciCredentialOfferPayload, OpenId4VciCredentialRequest, OpenId4VciCredentialSupported, OpenId4VciIssuerMetadataDisplay, } from '../shared' import type { AgentContext, W3cCredential } from '@aries-framework/core' import type { SdJwtVcSignOptions } from '@aries-framework/sd-jwt-vc' -import type { CredentialOfferPayloadV1_0_11 } from '@sphereon/oid4vci-common' export interface OpenId4VciPreAuthorizedCodeFlowConfig { preAuthorizedCode?: string @@ -54,7 +54,7 @@ export interface OpenId4VciCreateCredentialOfferOptions { // FIXME: this needs to be renamed, but will class with OpenId4VciCredentialOffer // Probably needs to be specific `XXReturn` type export type CredentialOffer = { - credentialOfferPayload: CredentialOfferPayloadV1_0_11 + credentialOfferPayload: OpenId4VciCredentialOfferPayload credentialOfferUri: string } diff --git a/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRecord.ts b/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRecord.ts index 30935554fd..0ad60a69bb 100644 --- a/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRecord.ts +++ b/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRecord.ts @@ -1,6 +1,5 @@ -import type { OpenId4VciCredentialSupportedWithId } from '../../shared' +import type { OpenId4VciCredentialSupportedWithId, OpenId4VciIssuerMetadataDisplay } from '../../shared' import type { RecordTags, TagsBase } from '@aries-framework/core' -import type { CredentialSupported, MetadataDisplay } from '@sphereon/oid4vci-common' import { BaseRecord, utils } from '@aries-framework/core' @@ -24,7 +23,7 @@ export interface OpenId4VcIssuerRecordProps { accessTokenPublicKeyFingerprint: string credentialsSupported: OpenId4VciCredentialSupportedWithId[] - display?: MetadataDisplay[] + display?: OpenId4VciIssuerMetadataDisplay[] } export class OpenId4VcIssuerRecord extends BaseRecord { @@ -34,8 +33,8 @@ export class OpenId4VcIssuerRecord extends BaseRecord { - return await this.openId4VcVerifierService.createProofRequest(this.agentContext, options) + public async getByVerifierId(verifierId: string) { + return this.openId4VcVerifierService.getByVerifierId(this.agentContext, verifierId) } /** - * Verifies a proof response with the provided options. - * The proof response validates the idToken, the signature of the received Verifiable Presentation, - * as well as that the structure of the Verifiable Presentation matches the provided presentation definition. + * Create a new verifier and store the new verifier record. + */ + public async createVerifier() { + return this.openId4VcVerifierService.createVerifier(this.agentContext) + } + + /** + * Create an authorization request, acting as a Relying Party (RP). + * + * Currently two types of requests are supported: + * - SIOP Self-Issued ID Token request: request to a Self-Issued OP from an RP + * - SIOP Verifiable Presentation Request: request to a Self-Issued OP from an RP, requesting a Verifiable Presentation using OpenID4VP * - * @param proofPayload - The payload of the proof response. - * @param options.createProofRequestOptions - The options used to create the proof request. - * @param options.proofRequestMetadata - Metadata about the proof request. - * @returns @see VerifiedProofResponse object containing the idTokenPayload and the verified submission. + * Other flows (non-SIOP) are not supported at the moment, but can be added in the future. + * + * See {@link OpenId4VcCreateAuthorizationRequestOptions} for detailed documentation on the options. */ - public async verifyProofResponse(proofPayload: ProofPayload): Promise { - return await this.openId4VcVerifierService.verifyProofResponse(this.agentContext, proofPayload) + public async createAuthorizationRequest({ + verifiedId, + ...otherOptions + }: OpenId4VcCreateAuthorizationRequestOptions & { + verifiedId: string + }): Promise { + const verifier = await this.getByVerifierId(verifiedId) + return await this.openId4VcVerifierService.createAuthorizationRequest(this.agentContext, { + ...otherOptions, + verifier, + }) } /** - * Configures the enabled endpoints for the given router, as specified in @link https://openid.net/specs/openid-4-verifiable-presentations-1_0.html + * Verifies an authorization response, acting as a Relying Party (RP). * - * @param router - The router to configure. - * @param endpointConfig - The endpoint configuration. - * @returns The configured router. + * It validates the ID Token, VP Token and the signature(s) of the received Verifiable Presentation(s) + * as well as that the structure of the Verifiable Presentation matches the provided presentation definition. */ - public async configureRouter(router: Router, endpointConfig: VerifierEndpointConfig) { - return await this.openId4VcVerifierService.configureRouter(this.agentContext, router, endpointConfig) + public async verifyAuthorizationResponse({ + verifierId, + ...otherOptions + }: OpenId4VcVerifyAuthorizationResponseOptions & { + verifierId: string + }): Promise { + const verifier = await this.getByVerifierId(verifierId) + return await this.openId4VcVerifierService.verifyProofResponse(this.agentContext, { + ...otherOptions, + verifier, + }) } } diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts index 9e01ad5783..e6ee9733df 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts @@ -1,18 +1,22 @@ import type { OpenId4VcVerifierModuleConfigOptions } from './OpenId4VcVerifierModuleConfig' -import type { DependencyManager, Module } from '@aries-framework/core' +import type { OpenId4VcVerificationRequest } from './router' +import type { AgentContext, DependencyManager, Module } from '@aries-framework/core' import { AgentConfig } from '@aries-framework/core' +import { getAgentContextForActorId, getRequestContext, importExpress } from '../shared/router' + import { OpenId4VcVerifierApi } from './OpenId4VcVerifierApi' import { OpenId4VcVerifierModuleConfig } from './OpenId4VcVerifierModuleConfig' import { OpenId4VcVerifierService } from './OpenId4VcVerifierService' +import { OpenId4VcVerifierRepository } from './repository' +import { configureAuthorizationEndpoint } from './router' /** * @public */ export class OpenId4VcVerifierModule implements Module { public readonly api = OpenId4VcVerifierApi - public readonly config: OpenId4VcVerifierModuleConfig public constructor(options: OpenId4VcVerifierModuleConfigOptions) { @@ -37,5 +41,72 @@ export class OpenId4VcVerifierModule implements Module { // Services dependencyManager.registerSingleton(OpenId4VcVerifierService) + + // Repository + dependencyManager.registerSingleton(OpenId4VcVerifierRepository) + } + + public async initialize(rootAgentContext: AgentContext): Promise { + this.configureRouter(rootAgentContext) + } + + /** + * Registers the endpoints on the router passed to this module. + */ + private configureRouter(rootAgentContext: AgentContext) { + const { Router, json, urlencoded } = importExpress() + + // We use separate context router and endpoint router. Context router handles the linking of the request + // to a specific agent context. Endpoint router only knows about a single context + const endpointRouter = Router() + const contextRouter = this.config.router + + // parse application/x-www-form-urlencoded + contextRouter.use(urlencoded({ extended: false })) + // parse application/json + contextRouter.use(json()) + + contextRouter.param('verifierId', async (req: OpenId4VcVerificationRequest, _res, next, verifierId: string) => { + if (!verifierId) { + _res.status(404).send('Not found') + } + + let agentContext: AgentContext | undefined = undefined + + try { + agentContext = await getAgentContextForActorId(rootAgentContext, verifierId) + const verifierApi = agentContext.dependencyManager.resolve(OpenId4VcVerifierApi) + const verifier = await verifierApi.getByVerifierId(verifierId) + + req.requestContext = { + agentContext, + verifier, + } + } catch (error) { + agentContext?.config.logger.error( + 'Failed to correlate incoming openid request to existing tenant and verifier', + { + error, + } + ) + // If the opening failed + await agentContext?.endSession() + return _res.status(404).send('Not found') + } + + next() + }) + + contextRouter.use('/:verifierId', endpointRouter) + + // Configure endpoints + configureAuthorizationEndpoint(endpointRouter, this.config.authorizationEndpoint) + + // FIXME: Will this be called when an error occurs / 404 is returned earlier on? + contextRouter.use(async (req: OpenId4VcVerificationRequest, _res, next) => { + const { agentContext } = getRequestContext(req) + await agentContext.endSession() + next() + }) } } diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts index 3abe7fc372..bc29abc1c5 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts @@ -1,18 +1,43 @@ -import type { VerifierMetadata } from './OpenId4VcVerifierServiceOptions' +import type { AuthorizationEndpointConfig } from './router/authorizationEndpoint' +import type { Optional, AgentContext } from '@aries-framework/core' +import type { Router } from 'express' -import { AgentConfig, type AgentContext } from '@aries-framework/core' +import { AgentConfig } from '@aries-framework/core' import { EventEmitter } from 'events' +import { importExpress } from '../shared/router' + import { InMemoryVerifierSessionManager, type IInMemoryVerifierSessionManager } from './InMemoryVerifierSessionManager' export interface OpenId4VcVerifierModuleConfigOptions { - verifierMetadata: VerifierMetadata + /** + * Base url at which the verifier endpoints will be hosted. All endpoints will be exposed with + * this path as prefix. + */ + baseUrl: string + + /** + * Express router on which the verifier endpoints will be registered. If + * no router is provided, a new one will be created. + * + * NOTE: you must manually register the router on your express app and + * expose this on a public url that is reachable when `baseUrl` is called. + */ + router?: Router + + endpoints: { + // FIXME: interface name with openid4vc prefix + authorization?: Optional + } + + // FIXME: remove sessionManagerFactory?: () => IInMemoryVerifierSessionManager } export class OpenId4VcVerifierModuleConfig { private options: OpenId4VcVerifierModuleConfigOptions - private basePathMap: Map + public readonly router: Router + private eventEmitterMap: Map private sessionManagerMap: Map @@ -20,7 +45,22 @@ export class OpenId4VcVerifierModuleConfig { this.options = options this.sessionManagerMap = new Map() this.eventEmitterMap = new Map() - this.basePathMap = new Map() + + this.router = options.router ?? importExpress().Router() + } + + public get baseUrl() { + return this.options.baseUrl + } + + public get authorizationEndpoint(): AuthorizationEndpointConfig { + // Use user supplied options, or return defaults. + const userOptions = this.options.endpoints.authorization + + return { + ...userOptions, + endpointPath: userOptions?.endpointPath ?? '/authorize', + } } public getSessionManager(agentContext: AgentContext) { @@ -44,16 +84,4 @@ export class OpenId4VcVerifierModuleConfig { this.eventEmitterMap.set(agentConext.contextCorrelationId, newVal) return newVal } - - public getBasePath(agentContext: AgentContext): string { - return this.basePathMap.get(agentContext.contextCorrelationId) ?? '/' - } - - public setBasePath(agentContext: AgentContext, basePath: string): void { - this.basePathMap.set(agentContext.contextCorrelationId, basePath) - } - - public get verifierMetadata() { - return this.options.verifierMetadata - } } diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierServiceOptions.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierServiceOptions.ts index 4686105980..c3f8a88e5b 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierServiceOptions.ts @@ -1,17 +1,11 @@ import type { VerificationMethod } from '@aries-framework/core' -import type { PresentationDefinitionV1, PresentationDefinitionV2 } from '@sphereon/pex-models' - -import { - type IDTokenPayload, - type VerifiedOpenID4VPSubmission, - type ClientMetadataOpts, - type AuthorizationResponsePayload, - ResponseType, - Scope, - PassBy, - SigningAlgo, - SubjectType, +import type { + IDTokenPayload, + VerifiedOpenID4VPSubmission, + ClientMetadataOpts, + AuthorizationResponsePayload, } from '@sphereon/did-auth-siop' +import type { PresentationDefinitionV1, PresentationDefinitionV2 } from '@sphereon/pex-models' export { PassBy, SigningAlgo, SubjectType, ResponseType, Scope } from '@sphereon/did-auth-siop' @@ -19,83 +13,67 @@ export type HolderMetadata = ClientMetadataOpts & { authorization_endpoint?: str export type { PresentationDefinitionV1, PresentationDefinitionV2, VerifiedOpenID4VPSubmission, IDTokenPayload } -export interface VerifierMetadata { - verifierBaseUrl: string - verificationEndpointPath: string -} - -export interface CreateProofRequestOptions { +export interface OpenId4VcCreateAuthorizationRequestOptions { + /** + * FIXME: should be a string or a VerificationMethod instance (or something we configure in the record?) + * The VerificationMethod used for signing the proof request. + */ verificationMethod: VerificationMethod + + /** + * FIXME: rework + * The URL to where the holder will send the response. + */ verificationEndpointUrl?: string /** - * The holder metadata to use for the proof request. - * If not provided, a static set of configuration values defined in the spec will be used. - * If provided as a string (url), it will try to retrieve the metadata from the given url. + * The OpenID Provider (OP) configuration can be provided in three different ways: + * - Statically, by providing the configuration as an object conforming to {@link HolderMetadata} in the `options.openIdProvider` parameter. + * - Dynamically, by providing the openid `issuer` URL in the `options.openIdProvider` parameter. The metadata will be retrieved + * from the hosted OpenID configuration endpoint. + * - Using a static configuration as defined in [SIOPv2](https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-static-configuration-values). The following + * static configurations are supported, identified by the value of the `authorization_endpoint`: + * - `siopv2:` - Supporting `id_token` as a `response_type`. See [`siopv2:`](https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-a-set-of-static-configurati) + * - `openid:` - Supporting both `vp_token` and `id_token` as a `response_type`. See [`openid:`](https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-a-set-of-static-configuratio) + * + * Note that `siopv2:` CAN NOT be used if a presentation definition is provided in the `presentationDefinition` parameter. If no value is supplied, the following defaults will be used as the `openIdProvider`: + * - `siopv2:` - If no presentation definition is provided + * - `openid:` - If a presentation definition is provided. */ - holderMetadata?: HolderMetadata | string - presentationDefinition?: PresentationDefinitionV1 | PresentationDefinitionV2 + openIdProvider?: HolderMetadata | string + + /** + * A DIF Presentation Definition (v2) can be provided to request a Verifiable Presentation using OpenID4VP. + */ + presentationDefinition?: PresentationDefinitionV2 } -export interface ProofRequestMetadata { +export interface OpenId4VcVerifyAuthorizationResponseOptions { + /** + * The authorization response received from the OpenID Provider (OP). + */ + authorizationResponse: OpenId4VcAuthorizationResponse +} + +export interface OpenId4VcAuthorizationRequestMetadata { correlationId: string - challenge: string + nonce: string state: string } -export type ProofRequestWithMetadata = { - proofRequest: string - proofRequestMetadata: ProofRequestMetadata +export interface OpenId4VcAuthorizationRequestWithMetadata { + authorizationRequestUri: string + metadata: OpenId4VcAuthorizationRequestMetadata } export interface VerifyProofResponseOptions { - createProofRequestOptions: CreateProofRequestOptions - proofRequestMetadata: ProofRequestMetadata + createProofRequestOptions: OpenId4VcCreateAuthorizationRequestOptions + proofRequestMetadata: OpenId4VcAuthorizationRequestMetadata } -export interface VerifiedProofResponse { +export interface VerifiedOpenId4VcAuthorizationResponse { idTokenPayload: IDTokenPayload submission: VerifiedOpenID4VPSubmission | undefined } -export type ProofPayload = AuthorizationResponsePayload - -export const staticOpSiopConfig: HolderMetadata = { - authorization_endpoint: 'siopv2:', - subject_syntax_types_supported: ['urn:ietf:params:oauth:jwk-thumbprint'], - responseTypesSupported: [ResponseType.ID_TOKEN], - scopesSupported: [Scope.OPENID], - subjectTypesSupported: [SubjectType.PAIRWISE], - idTokenSigningAlgValuesSupported: [SigningAlgo.ES256], - requestObjectSigningAlgValuesSupported: [SigningAlgo.ES256], - passBy: PassBy.VALUE, -} - -export const staticOpOpenIdConfig: HolderMetadata = { - authorization_endpoint: 'openid:', - subject_syntax_types_supported: ['urn:ietf:params:oauth:jwk-thumbprint'], - responseTypesSupported: [ResponseType.ID_TOKEN, ResponseType.VP_TOKEN], - scopesSupported: [Scope.OPENID], - subjectTypesSupported: [SubjectType.PAIRWISE], - idTokenSigningAlgValuesSupported: [SigningAlgo.ES256], - requestObjectSigningAlgValuesSupported: [SigningAlgo.ES256], - passBy: PassBy.VALUE, - vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.ES256] }, jwt_vp: { alg: [SigningAlgo.ES256] } }, -} - -export type ProofResponseHandlerReturn = { status: number } -export type ProofResponseHandler = (verifiedProofResponse: VerifiedProofResponse) => Promise - -export interface VerificationEndpointConfig { - /** - * Configures the router to expose the verification endpoint. - */ - enabled: boolean - - proofResponseHandler?: ProofResponseHandler -} - -export interface VerifierEndpointConfig { - basePath: string - verificationEndpointConfig: VerificationEndpointConfig -} +export type OpenId4VcAuthorizationResponse = AuthorizationResponsePayload diff --git a/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcVerifierRecord.ts b/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcVerifierRecord.ts new file mode 100644 index 0000000000..e493faa2d6 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcVerifierRecord.ts @@ -0,0 +1,44 @@ +import type { RecordTags, TagsBase } from '@aries-framework/core' + +import { BaseRecord, utils } from '@aries-framework/core' + +export type OpenId4VcVerifierRecordTags = RecordTags + +export type DefaultOpenId4VcVerifierRecordTags = { + verifierId: string +} + +export interface OpenId4VcVerifierRecordProps { + id?: string + createdAt?: Date + tags?: TagsBase + + verifierId: string +} + +// FIXME: combine with issuer record? +export class OpenId4VcVerifierRecord extends BaseRecord { + public static readonly type = 'OpenId4VcVerifierRecord' + public readonly type = OpenId4VcVerifierRecord.type + + public verifierId!: string + + public constructor(props: OpenId4VcVerifierRecordProps) { + super() + + if (props) { + this.id = props.id ?? utils.uuid() + this.createdAt = props.createdAt ?? new Date() + this._tags = props.tags ?? {} + + this.verifierId = props.verifierId + } + } + + public getTags() { + return { + ...this._tags, + verifierId: this.verifierId, + } + } +} diff --git a/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcVerifierRepository.ts b/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcVerifierRepository.ts new file mode 100644 index 0000000000..57622b6de8 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcVerifierRepository.ts @@ -0,0 +1,23 @@ +import type { AgentContext } from '@aries-framework/core' + +import { Repository, StorageService, InjectionSymbols, EventEmitter, inject, injectable } from '@aries-framework/core' + +import { OpenId4VcVerifierRecord } from './OpenId4VcVerifierRecord' + +@injectable() +export class OpenId4VcVerifierRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(OpenId4VcVerifierRecord, storageService, eventEmitter) + } + + public findByVerifierId(agentContext: AgentContext, verifierId: string) { + return this.findSingleByQuery(agentContext, { verifierId }) + } + + public getByVerifierId(agentContext: AgentContext, verifierId: string) { + return this.getSingleByQuery(agentContext, { verifierId }) + } +} diff --git a/packages/openid4vc/src/openid4vc-verifier/repository/index.ts b/packages/openid4vc/src/openid4vc-verifier/repository/index.ts new file mode 100644 index 0000000000..09bef0307e --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/repository/index.ts @@ -0,0 +1,2 @@ +export * from './OpenId4VcVerifierRecord' +export * from './OpenId4VcVerifierRepository' diff --git a/packages/openid4vc/src/openid4vc-verifier/router/OpenId4VpEndpointConfiguration.ts b/packages/openid4vc/src/openid4vc-verifier/router/OpenId4VpEndpointConfiguration.ts deleted file mode 100644 index 1d1bc95d14..0000000000 --- a/packages/openid4vc/src/openid4vc-verifier/router/OpenId4VpEndpointConfiguration.ts +++ /dev/null @@ -1,47 +0,0 @@ -import type { OpenId4VcVerifierService } from '../OpenId4VcVerifierService' -import type { VerificationEndpointConfig } from '../OpenId4VcVerifierServiceOptions' -import type { AgentContext, Logger } from '@aries-framework/core' -import type { AuthorizationResponsePayload } from '@sphereon/did-auth-siop' -import type { Router, Request } from 'express' - -import { getRequestContext, sendErrorResponse } from '../../shared/router' - -export interface VerificationRequestContext { - agentContext: AgentContext - openId4VcVerifierService: OpenId4VcVerifierService - logger: Logger -} - -export interface VerificationRequest extends Request { - requestContext?: VerificationRequestContext -} - -export type InternalVerificationEndpointConfig = VerificationEndpointConfig - -export const configureVerificationEndpoint = ( - router: Router, - pathname: string, - config: InternalVerificationEndpointConfig -) => { - router.post(pathname, async (request: VerificationRequest, response) => { - const { logger, agentContext, openId4VcVerifierService } = getRequestContext(request) - try { - const isVpRequest = request.body.presentation_submission !== undefined - - const authorizationResponse: AuthorizationResponsePayload = request.body - if (isVpRequest) authorizationResponse.presentation_submission = JSON.parse(request.body.presentation_submission) - - const verifiedProofResponse = await openId4VcVerifierService.verifyProofResponse(agentContext, request.body) - if (!config.proofResponseHandler) return response.status(200).send() - - const { status } = await config.proofResponseHandler(verifiedProofResponse) - return response.status(status).send() - } catch (error: unknown) { - sendErrorResponse(response, logger, 500, 'invalid_request', error) - } - - return response.status(200).send() - }) - - return router -} diff --git a/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts b/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts new file mode 100644 index 0000000000..552a868471 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts @@ -0,0 +1,36 @@ +import type { OpenId4VcVerificationRequest } from './requestContext' +import type { AuthorizationResponsePayload } from '@sphereon/did-auth-siop' +import type { Router, Response } from 'express' + +import { getRequestContext, sendErrorResponse } from '../../shared/router' +import { OpenId4VcVerifierService } from '../OpenId4VcVerifierService' + +export interface AuthorizationEndpointConfig { + /** + * The path at which the authorization endpoint should be made available. Note that it will be + * hosted at a subpath to take into account multiple tenants and verifiers. + * + * @default /authorize + */ + endpointPath: string +} + +export function configureAuthorizationEndpoint(router: Router, config: AuthorizationEndpointConfig) { + router.post(config.endpointPath, async (request: OpenId4VcVerificationRequest, response: Response) => { + const { agentContext } = getRequestContext(request) + + try { + const openId4VcVerifierService = agentContext.dependencyManager.resolve(OpenId4VcVerifierService) + const isVpRequest = request.body.presentation_submission !== undefined + + // FIXME: body should be verified + const authorizationResponse: AuthorizationResponsePayload = request.body + if (isVpRequest) authorizationResponse.presentation_submission = JSON.parse(request.body.presentation_submission) + + await openId4VcVerifierService.verifyAuthorizationResponse(agentContext, request.body) + return response.status(200).send() + } catch (error) { + return sendErrorResponse(response, agentContext.config.logger, 500, 'invalid_request', error) + } + }) +} diff --git a/packages/openid4vc/src/openid4vc-verifier/router/index.ts b/packages/openid4vc/src/openid4vc-verifier/router/index.ts new file mode 100644 index 0000000000..8242556be4 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/router/index.ts @@ -0,0 +1,2 @@ +export { configureAuthorizationEndpoint } from './authorizationEndpoint' +export { OpenId4VcVerificationRequest } from './requestContext' diff --git a/packages/openid4vc/src/openid4vc-verifier/router/requestContext.ts b/packages/openid4vc/src/openid4vc-verifier/router/requestContext.ts new file mode 100644 index 0000000000..4dcb3964d8 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/router/requestContext.ts @@ -0,0 +1,4 @@ +import type { OpenId4VcRequest } from '../../shared/router' +import type { OpenId4VcVerifierRecord } from '../repository' + +export type OpenId4VcVerificationRequest = OpenId4VcRequest<{ verifier: OpenId4VcVerifierRecord }> diff --git a/packages/openid4vc/src/openid4vc-verifier/staticOpConfiguration.ts b/packages/openid4vc/src/openid4vc-verifier/staticOpConfiguration.ts new file mode 100644 index 0000000000..d5a526b3b7 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/staticOpConfiguration.ts @@ -0,0 +1,26 @@ +import type { HolderMetadata } from './OpenId4VcVerifierServiceOptions' + +import { ResponseType, Scope, SubjectType, SigningAlgo, PassBy } from '@sphereon/did-auth-siop' + +export const siopv2StaticOpConfiguration: HolderMetadata = { + authorization_endpoint: 'siopv2:', + subject_syntax_types_supported: ['urn:ietf:params:oauth:jwk-thumbprint'], + responseTypesSupported: [ResponseType.ID_TOKEN], + scopesSupported: [Scope.OPENID], + subjectTypesSupported: [SubjectType.PAIRWISE], + idTokenSigningAlgValuesSupported: [SigningAlgo.ES256], + requestObjectSigningAlgValuesSupported: [SigningAlgo.ES256], + passBy: PassBy.VALUE, +} + +export const openidStaticOpConfiguration: HolderMetadata = { + authorization_endpoint: 'openid:', + subject_syntax_types_supported: ['urn:ietf:params:oauth:jwk-thumbprint'], + responseTypesSupported: [ResponseType.ID_TOKEN, ResponseType.VP_TOKEN], + scopesSupported: [Scope.OPENID], + subjectTypesSupported: [SubjectType.PAIRWISE], + idTokenSigningAlgValuesSupported: [SigningAlgo.ES256], + requestObjectSigningAlgValuesSupported: [SigningAlgo.ES256], + passBy: PassBy.VALUE, + vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.ES256] }, jwt_vp: { alg: [SigningAlgo.ES256] } }, +} diff --git a/packages/openid4vc/src/shared/index.ts b/packages/openid4vc/src/shared/index.ts index ad200c539f..8eacb927b2 100644 --- a/packages/openid4vc/src/shared/index.ts +++ b/packages/openid4vc/src/shared/index.ts @@ -1 +1,2 @@ export * from './models' +export * from './issuerMetadataUtils' diff --git a/packages/openid4vc/src/shared/issuerMetadataUtils.ts b/packages/openid4vc/src/shared/issuerMetadataUtils.ts new file mode 100644 index 0000000000..076d8361a6 --- /dev/null +++ b/packages/openid4vc/src/shared/issuerMetadataUtils.ts @@ -0,0 +1,83 @@ +import type { OpenId4VciCredentialSupported, OpenId4VciCredentialSupportedWithId } from './models' +import type { AuthorizationDetails, CredentialOfferFormat, EndpointMetadataResult } from '@sphereon/oid4vci-common' + +import { AriesFrameworkError } from '@aries-framework/core' + +/** + * Get all `types` from a `CredentialSupported` object. + * + * Depending on the format, the types may be nested, or have different a different name/type + */ +export function getTypesFromCredentialSupported(credentialSupported: OpenId4VciCredentialSupported) { + if ( + credentialSupported.format === 'jwt_vc_json-ld' || + credentialSupported.format === 'ldp_vc' || + credentialSupported.format === 'jwt_vc_json' || + credentialSupported.format === 'jwt_vc' + ) { + return credentialSupported.types + } else if (credentialSupported.format === 'vc+sd-jwt') { + return [credentialSupported.vct] + } + + throw Error(`Unable to extract types from credentials supported. Unknown format ${credentialSupported.format}`) +} + +/** + * Returns all entries from the credential offer with the associated metadata resolved. For 'id' entries, the associated `credentials_supported` object is resolved from the issuer metadata. + * For inline entries, an error is thrown. + */ +export function getOfferedCredentials( + offeredCredentials: Array, + allCredentialsSupported: OpenId4VciCredentialSupported[] +): OpenId4VciCredentialSupportedWithId[] { + const credentialsSupported: OpenId4VciCredentialSupportedWithId[] = [] + + for (const offeredCredential of offeredCredentials) { + // In draft 12 inline credential offers are removed. It's easier to already remove support now. + if (typeof offeredCredential !== 'string') { + throw new AriesFrameworkError( + 'Only referenced credentials pointing to an id in credentials_supported issuer metadata are supported' + ) + } + + const foundSupportedCredential = allCredentialsSupported.find( + (supportedCredential): supportedCredential is OpenId4VciCredentialSupportedWithId => + supportedCredential.id !== undefined && supportedCredential.id === offeredCredential + ) + + // Make sure the issuer metadata includes the offered credential. + if (!foundSupportedCredential) { + throw new Error( + `Offered credential '${offeredCredential}' is not part of credentials_supported of the issuer metadata.` + ) + } + + credentialsSupported.push(foundSupportedCredential) + } + + return credentialsSupported +} + +// copied from sphereon as the method is only available on the client +export function handleAuthorizationDetails( + authorizationDetails: AuthorizationDetails | AuthorizationDetails[], + metadata: EndpointMetadataResult +): AuthorizationDetails | AuthorizationDetails[] | undefined { + if (Array.isArray(authorizationDetails)) { + return authorizationDetails.map((value) => handleLocations(value, metadata)) + } else { + return handleLocations(authorizationDetails, metadata) + } +} + +// copied from sphereon as the method is only available on the client +function handleLocations(authorizationDetails: AuthorizationDetails, metadata: EndpointMetadataResult) { + if (typeof authorizationDetails === 'string') return authorizationDetails + if (metadata.credentialIssuerMetadata?.authorization_server || metadata.authorization_endpoint) { + if (!authorizationDetails.locations) authorizationDetails.locations = [metadata.issuer] + else if (Array.isArray(authorizationDetails.locations)) authorizationDetails.locations.push(metadata.issuer) + else authorizationDetails.locations = [authorizationDetails.locations as string, metadata.issuer] + } + return authorizationDetails +} diff --git a/packages/openid4vc/src/shared/models/OpenId4VciCredentialFormatProfile.ts b/packages/openid4vc/src/shared/models/OpenId4VciCredentialFormatProfile.ts new file mode 100644 index 0000000000..628e65c12e --- /dev/null +++ b/packages/openid4vc/src/shared/models/OpenId4VciCredentialFormatProfile.ts @@ -0,0 +1,6 @@ +export enum OpenId4VciCredentialFormatProfile { + JwtVcJson = 'jwt_vc_json', + JwtVcJsonLd = 'jwt_vc_json-ld', + LdpVc = 'ldp_vc', + SdJwtVc = 'vc+sd-jwt', +} diff --git a/packages/openid4vc/src/shared/models/index.ts b/packages/openid4vc/src/shared/models/index.ts index ae0fa4a1ee..a20434cf4c 100644 --- a/packages/openid4vc/src/shared/models/index.ts +++ b/packages/openid4vc/src/shared/models/index.ts @@ -1,5 +1,7 @@ import type { AssertedUniformCredentialOffer, + CredentialIssuerMetadata, + CredentialOfferPayloadV1_0_11, CredentialRequestJwtVcJson, CredentialRequestJwtVcJsonLdAndLdpVc, CredentialRequestSdJwtVc, @@ -10,11 +12,14 @@ import type { export type OpenId4VciCredentialSupportedWithId = CredentialSupported & { id: string } export type OpenId4VciCredentialSupported = CredentialSupported +export type OpenId4VciIssuerMetadata = CredentialIssuerMetadata export type OpenId4VciIssuerMetadataDisplay = MetadataDisplay export type OpenId4VciCredentialRequest = UniformCredentialRequest export type OpenId4VciCredentialRequestJwtVcJson = CredentialRequestJwtVcJson export type OpenId4VciCredentialRequestJwtVcJsonLdAndLdpVc = CredentialRequestJwtVcJsonLdAndLdpVc export type OpenId4VciCredentialRequestSdJwtVc = CredentialRequestSdJwtVc export type OpenId4VciCredentialOffer = AssertedUniformCredentialOffer +export type OpenId4VciCredentialOfferPayload = CredentialOfferPayloadV1_0_11 export * from './CredentialHolderBinding' +export * from './OpenId4VciCredentialFormatProfile' From 78dd60bcdffb56e1f0dc5d772193e91d13bb9054 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sun, 21 Jan 2024 16:11:41 +0700 Subject: [PATCH 102/115] fix some issuer tests Signed-off-by: Timo Glastra --- .../OpenId4VcIssuerService.ts | 24 +- .../OpenId4VcIssuerServiceOptions.ts | 15 +- .../__tests__/openid4vc-issuer.e2e.test.ts | 597 ++++++++++-------- 3 files changed, 349 insertions(+), 287 deletions(-) diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts index b8f738a5a3..a2f8dc1b87 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts @@ -490,19 +490,19 @@ export class OpenId4VcIssuerService { ) } - let signOptions = options.credential - if (!signOptions) { - const holderBinding = await this.getHolderBindingFromRequest(credentialRequest) - signOptions = await this.openId4VcIssuerConfig.credentialEndpoint.credentialRequestToCredentialMapper({ - agentContext, - holderBinding, - - credentialOffer, - credentialRequest, + const holderBinding = await this.getHolderBindingFromRequest(credentialRequest) + const mapper = + options.credentialRequestToCredentialMapper ?? + this.openId4VcIssuerConfig.credentialEndpoint.credentialRequestToCredentialMapper + const signOptions = await mapper({ + agentContext, + holderBinding, + + credentialOffer, + credentialRequest, - credentialsSupported: offeredCredentialsMatchingRequest, - }) - } + credentialsSupported: offeredCredentialsMatchingRequest, + }) if (isW3cSignCredentialOptions(signOptions)) { if (!w3cOpenId4VcFormats.includes(credentialRequest.format as OpenId4VciCredentialFormatProfile)) { diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts index b60cc7c157..ce306725b3 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts @@ -63,13 +63,13 @@ export interface CreateCredentialResponseOptions { credentialRequest: OpenId4VciCredentialRequest /** - * You can optionally provide the input data for signing the credential. - * If not provided the `credentialRequestToCredentialMapper` from the module - * config will be called with needed data to construct the credential - * signing payload + * You can optionally provide a credential request to credential mapper that will be + * dynamically invoked to return credential data based on the credential request. + * + * If not provided, the `credentialRequestToCredentialMapper` from the agent config + * will be used. */ - // FIXME: credential.credential is not nice - credential?: OpenId4VciSignCredential + credentialRequestToCredentialMapper?: OpenId4VciCredentialRequestToCredentialMapper } // FIXME: openid4vc prefix for all interfaces @@ -103,7 +103,7 @@ export type OpenId4VciCredentialRequestToCredentialMapper = (options: { * NOTE: in v12 this will probably become a single entry, as it will be matched on id */ credentialsSupported: OpenId4VciCredentialSupported[] -}) => Promise +}) => Promise | OpenId4VciSignCredential // FIXME: can we make these interfaces more uniform or is it okay // to have quite some differences between them? I think the nice @@ -111,6 +111,7 @@ export type OpenId4VciCredentialRequestToCredentialMapper = (options: { // w3c and sd-jwt services. However in that case you could also // ask why not just require the signed credential as output // as you can then just call the services yourself. +// FIMXE: add type for type of credential. Also to input of mapper. W3c can be returned for jwt + ldp. and sd-jwt for vc+sd-jwt export type OpenId4VciSignCredential = OpenId4VciSignSdJwtCredential | OpenId4VciSignW3cCredential export type OpenId4VciSignSdJwtCredential = SdJwtVcSignOptions export interface OpenId4VciSignW3cCredential { diff --git a/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.e2e.test.ts b/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.e2e.test.ts index 0e6353ae9a..b5b365f68a 100644 --- a/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.e2e.test.ts @@ -1,9 +1,9 @@ +import type { OpenId4VciCredentialRequest, OpenId4VciCredentialSupportedWithId } from '../../shared' import type { - AuthorizationCodeFlowConfig, - CredentialSupported, - IssuerMetadata, - PreAuthorizedCodeFlowConfig, + OpenId4VcIssuerMetadata, + OpenId4VciCredentialRequestToCredentialMapper, } from '../OpenId4VcIssuerServiceOptions' +import type { OpenId4VcIssuerRecord } from '../repository' import type { AgentContext, KeyDidCreateOptions, @@ -11,11 +11,11 @@ import type { W3cVerifiableCredential, W3cVerifyCredentialResult, } from '@aries-framework/core' -import type { CredentialRequestV1_0_11 } from '@sphereon/oid4vci-common' import type { OriginalVerifiableCredential as SphereonW3cVerifiableCredential } from '@sphereon/ssi-types' import { AskarModule } from '@aries-framework/askar' import { + JwtPayload, Agent, AriesFrameworkError, DidKey, @@ -36,56 +36,54 @@ import { w3cDate, } from '@aries-framework/core' import { agentDependencies } from '@aries-framework/node' -import { SdJwtCredential, SdJwtVcApi, SdJwtVcModule } from '@aries-framework/sd-jwt-vc' +import { SdJwtVcApi, SdJwtVcModule } from '@aries-framework/sd-jwt-vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' -import { cleanAll, enableNetConnect } from 'nock' -import { OpenId4VcIssuerModule, OpenId4VcIssuerModuleConfig } from '..' -import { OpenIdCredentialFormatProfile } from '../../openid4vc-holder' - -type CredentialSupportedWithId = CredentialSupported & { id: string } +import { OpenId4VciCredentialFormatProfile } from '../../shared' +import { OpenId4VcIssuerModule } from '../OpenId4VcIssuerModule' +import { OpenId4VcIssuerModuleConfig } from '../OpenId4VcIssuerModuleConfig' const openBadgeCredential = { id: 'https://openid4vc-issuer.com/credentials/OpenBadgeCredential', - format: OpenIdCredentialFormatProfile.JwtVcJson, + format: OpenId4VciCredentialFormatProfile.JwtVcJson, types: ['VerifiableCredential', 'OpenBadgeCredential'], -} satisfies CredentialSupportedWithId +} satisfies OpenId4VciCredentialSupportedWithId const universityDegreeCredential = { id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredential', - format: OpenIdCredentialFormatProfile.JwtVcJson, + format: OpenId4VciCredentialFormatProfile.JwtVcJson, types: ['VerifiableCredential', 'UniversityDegreeCredential'], -} satisfies CredentialSupportedWithId +} satisfies OpenId4VciCredentialSupportedWithId const universityDegreeCredentialLd = { id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredentialLd', - format: OpenIdCredentialFormatProfile.JwtVcJsonLd, + format: OpenId4VciCredentialFormatProfile.JwtVcJsonLd, '@context': [], types: ['VerifiableCredential', 'UniversityDegreeCredential'], -} satisfies CredentialSupportedWithId +} satisfies OpenId4VciCredentialSupportedWithId const universityDegreeCredentialSdJwt = { id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredentialSdJwt', - format: OpenIdCredentialFormatProfile.SdJwtVc, - credential_definition: { - vct: 'UniversityDegreeCredential', - }, -} satisfies CredentialSupportedWithId + format: OpenId4VciCredentialFormatProfile.SdJwtVc, + vct: 'UniversityDegreeCredential', +} satisfies OpenId4VciCredentialSupportedWithId const baseCredentialRequestOptions = { scheme: 'openid-credential-offer', baseUri: 'openid4vc-issuer.com', } -const issuerMetadata: IssuerMetadata = { - issuerBaseUrl: 'https://openid4vc-issuer.com', - tokenEndpointPath: '/token', - credentialEndpointPath: '/credentials', - credentialsSupported: [openBadgeCredential, universityDegreeCredentialLd, universityDegreeCredentialSdJwt], -} - const modules = { - openId4VcIssuer: new OpenId4VcIssuerModule({ issuerMetadata }), + openId4VcIssuer: new OpenId4VcIssuerModule({ + baseUrl: 'https://openid4vc-issuer.com', + endpoints: { + credential: { + credentialRequestToCredentialMapper: () => { + throw new Error('Not implemented') + }, + }, + }, + }), sdJwtVc: new SdJwtVcModule(), askar: new AskarModule({ ariesAskar }), } @@ -95,17 +93,15 @@ const jwsService = new JwsService() const createCredentialRequest = async ( agentContext: AgentContext, options: { - issuerMetadata: IssuerMetadata - credentialSupported: CredentialSupportedWithId + issuerMetadata: OpenId4VcIssuerMetadata + credentialSupported: OpenId4VciCredentialSupportedWithId nonce: string kid: string clientId?: string // use with the authorization code flow, } -): Promise => { +): Promise => { const { credentialSupported, kid, nonce, issuerMetadata, clientId } = options - const aud = issuerMetadata.issuerBaseUrl - const didsApi = agentContext.dependencyManager.resolve(DidsApi) const didDocument = await didsApi.resolveDidDocument(kid) if (!didDocument.verificationMethod) { @@ -115,77 +111,72 @@ const createCredentialRequest = async ( const verificationMethod = didDocument.dereferenceKey(kid, ['authentication', 'assertionMethod']) const key = getKeyFromVerificationMethod(verificationMethod) const jwk = getJwkFromKey(key) - const alg = jwk.supportedSignatureAlgorithms[0] - - const rawPayload = { - iat: Math.floor(Date.now() / 1000), // unix time - iss: clientId, - aud, - nonce, - } - - const payload = TypedArrayEncoder.fromString(JSON.stringify(rawPayload)) - const typ = 'openid4vci-proof+jwt' const jws = await jwsService.createJwsCompact(agentContext, { - protectedHeaderOptions: { alg, kid, typ }, - payload, + protectedHeaderOptions: { alg: jwk.supportedSignatureAlgorithms[0], kid, typ: 'openid4vci-proof+jwt' }, + payload: new JwtPayload({ + iat: Math.floor(Date.now() / 1000), // unix time + iss: clientId, + aud: issuerMetadata.issuerUrl, + additionalClaims: { + nonce, + }, + }), key, }) - if (credentialSupported.format === OpenIdCredentialFormatProfile.JwtVcJson) { + if (credentialSupported.format === OpenId4VciCredentialFormatProfile.JwtVcJson) { return { ...credentialSupported, proof: { jwt: jws, proof_type: 'jwt' } } } else if ( - credentialSupported.format === OpenIdCredentialFormatProfile.JwtVcJsonLd || - credentialSupported.format === OpenIdCredentialFormatProfile.LdpVc + credentialSupported.format === OpenId4VciCredentialFormatProfile.JwtVcJsonLd || + credentialSupported.format === OpenId4VciCredentialFormatProfile.LdpVc ) { return { format: credentialSupported.format, credential_definition: { '@context': credentialSupported['@context'], types: credentialSupported.types }, proof: { jwt: jws, proof_type: 'jwt' }, } - } else if (credentialSupported.format === OpenIdCredentialFormatProfile.SdJwtVc) { + } else if (credentialSupported.format === OpenId4VciCredentialFormatProfile.SdJwtVc) { return { ...credentialSupported, proof: { jwt: jws, proof_type: 'jwt' } } } throw new Error('Unsupported format') } +const issuer = new Agent({ + config: { + label: 'OpenId4VcIssuer Test323', + walletConfig: { + id: 'openid4vc-Issuer-test323', + key: 'openid4vc-Issuer-test323', + }, + }, + dependencies: agentDependencies, + modules, +}) + +const holder = new Agent({ + config: { + label: 'OpenId4VciIssuer(Holder) Test323', + walletConfig: { + id: 'openid4vc-Issuer(Holder)-test323', + key: 'openid4vc-Issuer(Holder)-test323', + }, + }, + dependencies: agentDependencies, + modules, +}) + describe('OpenId4VcIssuer', () => { - let issuer: Agent let issuerVerificationMethod: VerificationMethod let issuerDid: string + let openId4VcIssuer: OpenId4VcIssuerRecord - let holder: Agent let holderKid: string let holderVerificationMethod: VerificationMethod let holderDid: string beforeEach(async () => { - issuer = new Agent({ - config: { - label: 'OpenId4VcIssuer Test323', - walletConfig: { - id: 'openid4vc-Issuer-test323', - key: 'openid4vc-Issuer-test323', - }, - }, - dependencies: agentDependencies, - modules, - }) - - holder = new Agent({ - config: { - label: 'OpenId4VciIssuer(Holder) Test323', - walletConfig: { - id: 'openid4vc-Issuer(Holder)-test323', - key: 'openid4vc-Issuer(Holder)-test323', - }, - }, - dependencies: agentDependencies, - modules, - }) - await issuer.initialize() await holder.initialize() @@ -219,6 +210,15 @@ describe('OpenId4VcIssuer', () => { ]) if (!_issuerVerificationMethod) throw new Error('No verification method found') issuerVerificationMethod = _issuerVerificationMethod + + openId4VcIssuer = await issuer.modules.openId4VcIssuer.createIssuer({ + credentialsSupported: [ + openBadgeCredential, + universityDegreeCredential, + universityDegreeCredentialLd, + universityDegreeCredentialSdJwt, + ], + }) }) afterEach(async () => { @@ -227,19 +227,18 @@ describe('OpenId4VcIssuer', () => { await holder.shutdown() await holder.wallet.delete() - - cleanAll() - enableNetConnect() }) + // This method is available on the holder service, + // would be nice to reuse async function handleCredentialResponse( agentContext: AgentContext, sphereonVerifiableCredential: SphereonW3cVerifiableCredential, - credentialSupported: CredentialSupportedWithId + credentialSupported: OpenId4VciCredentialSupportedWithId ) { if (credentialSupported.format === 'vc+sd-jwt' && typeof sphereonVerifiableCredential === 'string') { const api = agentContext.dependencyManager.resolve(SdJwtVcApi) - await api.fromSerializedJwt(sphereonVerifiableCredential, { holderDidUrl: holderKid }) + await api.verify({ compactSdJwtVc: sphereonVerifiableCredential }) return } @@ -275,54 +274,75 @@ describe('OpenId4VcIssuer', () => { return w3cVerifiableCredential } - it('pre authorized code flow (sdjwtvc)', async () => { + it('pre authorized code flow (sd-jwt-vc)', async () => { const cNonce = '1234' const preAuthorizedCode = '1234567890' - await issuer.context.dependencyManager - .resolve(OpenId4VcIssuerModuleConfig) + await issuer.modules.openId4VcIssuer.config .getCNonceStateManager(issuer.context) .set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) - const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, userPinRequired: false } + const result = await issuer.modules.openId4VcIssuer.createCredentialOffer({ + issuerId: openId4VcIssuer.issuerId, + offeredCredentials: [universityDegreeCredentialSdJwt.id], + preAuthorizedCodeFlowConfig: { + preAuthorizedCode, + userPinRequired: false, + }, + // FIXME: can we take the base uri from the config? Do we want to provide this? + ...baseCredentialRequestOptions, + }) - const result = await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest( - [universityDegreeCredentialSdJwt.id], - { - preAuthorizedCodeFlowConfig, - ...baseCredentialRequestOptions, - } - ) + expect(result.credentialOfferPayload).toEqual({ + credential_issuer: `https://openid4vc-issuer.com/${openId4VcIssuer.issuerId}`, + credentials: ['https://openid4vc-issuer.com/credentials/UniversityDegreeCredentialSdJwt'], + grants: { + authorization_code: undefined, + 'urn:ietf:params:oauth:grant-type:pre-authorized_code': { + 'pre-authorized_code': '1234567890', + user_pin_required: false, + }, + }, + }) - expect(result.credentialOfferRequest).toEqual( - 'openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FUniversityDegreeCredentialSdJwt%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%22%7D' + expect(result.credentialOfferUri).toEqual( + `openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FUniversityDegreeCredentialSdJwt%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` ) - const sdJwtCredential = new SdJwtCredential({ - payload: { type: 'UniversityDegreeCredential', university: 'innsbruck', degree: 'bachelor' }, - holderDidUrl: holderVerificationMethod.id, - issuerDidUrl: issuerVerificationMethod.id, - disclosureFrame: { university: true, degree: true }, + const issuerMetadata = await issuer.modules.openId4VcIssuer.getIssuerMetadata(openId4VcIssuer.issuerId) + const credentialRequest = await createCredentialRequest(holder.context, { + credentialSupported: universityDegreeCredentialSdJwt, + issuerMetadata, + kid: holderKid, + nonce: cNonce, }) - const issueCredentialResponse = await issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ - credential: sdJwtCredential, - verificationMethod: issuerVerificationMethod, - credentialRequest: await createCredentialRequest(holder.context, { - credentialSupported: universityDegreeCredentialSdJwt, - issuerMetadata, - kid: holderKid, - nonce: cNonce, + const issueCredentialResponse = await issuer.modules.openId4VcIssuer.createCredentialResponse({ + issuerId: openId4VcIssuer.issuerId, + credentialRequest, + + credentialRequestToCredentialMapper: () => ({ + payload: { vct: 'UniversityDegreeCredential', university: 'innsbruck', degree: 'bachelor' }, + issuer: { method: 'did', didUrl: issuerVerificationMethod.id }, + holder: { method: 'did', didUrl: holderVerificationMethod.id }, + disclosureFrame: { university: true, degree: true }, }), }) const sphereonW3cCredential = issueCredentialResponse.credential if (!sphereonW3cCredential) throw new Error('No credential found') + expect(issueCredentialResponse).toEqual({ + c_nonce: expect.any(String), + c_nonce_expires_in: 300000, + credential: expect.any(String), + format: 'vc+sd-jwt', + }) + await handleCredentialResponse(holder.context, sphereonW3cCredential, universityDegreeCredentialSdJwt) }) - it('pre authorized code flow (jwtvcjson)', async () => { + it('pre authorized code flow (jwt-vc-json)', async () => { const cNonce = '1234' const preAuthorizedCode = '1234567890' @@ -331,27 +351,32 @@ describe('OpenId4VcIssuer', () => { .getCNonceStateManager(issuer.context) .set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) - const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, userPinRequired: false } - - const result = await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest([openBadgeCredential.id], { - preAuthorizedCodeFlowConfig, + const result = await issuer.modules.openId4VcIssuer.createCredentialOffer({ + issuerId: openId4VcIssuer.issuerId, + offeredCredentials: [openBadgeCredential.id], + preAuthorizedCodeFlowConfig: { + preAuthorizedCode, + userPinRequired: false, + }, ...baseCredentialRequestOptions, }) - expect(result.credentialOfferRequest).toEqual( - 'openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%22%7D' + expect(result.credentialOfferUri).toEqual( + `openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` ) - const credential = new W3cCredential({ - type: openBadgeCredential.types, - issuer: new W3cIssuer({ id: issuerDid }), - credentialSubject: new W3cCredentialSubject({ id: holderDid }), - issuanceDate: w3cDate(Date.now()), - }) - - const issueCredentialResponse = await issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ - credential, - verificationMethod: issuerVerificationMethod, + const issuerMetadata = await issuer.modules.openId4VcIssuer.getIssuerMetadata(openId4VcIssuer.issuerId) + const issueCredentialResponse = await issuer.modules.openId4VcIssuer.createCredentialResponse({ + issuerId: openId4VcIssuer.issuerId, + credentialRequestToCredentialMapper: () => ({ + credential: new W3cCredential({ + type: openBadgeCredential.types, + issuer: new W3cIssuer({ id: issuerDid }), + credentialSubject: new W3cCredentialSubject({ id: holderDid }), + issuanceDate: w3cDate(Date.now()), + }), + verificationMethod: issuerVerificationMethod.id, + }), credentialRequest: await createCredentialRequest(holder.context, { credentialSupported: openBadgeCredential, issuerMetadata, @@ -363,6 +388,13 @@ describe('OpenId4VcIssuer', () => { const sphereonW3cCredential = issueCredentialResponse.credential if (!sphereonW3cCredential) throw new Error('No credential found') + expect(issueCredentialResponse).toEqual({ + c_nonce: expect.any(String), + c_nonce_expires_in: 300000, + credential: expect.any(String), + format: 'jwt_vc_json', + }) + await handleCredentialResponse(holder.context, sphereonW3cCredential, openBadgeCredential) }) @@ -375,18 +407,19 @@ describe('OpenId4VcIssuer', () => { .getCNonceStateManager(issuer.context) .set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) - const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, userPinRequired: false } - await expect( - issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest(['invalid id'], { - //issuerMetadata: { - // ...baseIssuerMetadata, - // credentialsSupported: [openBadgeCredential, universityDegreeCredential], - //}, - preAuthorizedCodeFlowConfig, + issuer.modules.openId4VcIssuer.createCredentialOffer({ + issuerId: openId4VcIssuer.issuerId, + offeredCredentials: ['invalid id'], + preAuthorizedCodeFlowConfig: { + preAuthorizedCode, + userPinRequired: false, + }, ...baseCredentialRequestOptions, }) - ).rejects.toThrowError() + ).rejects.toThrowError( + "Offered credential 'invalid id' is not part of credentials_supported of the issuer metadata." + ) }) it('issuing non offered credential errors', async () => { @@ -398,36 +431,36 @@ describe('OpenId4VcIssuer', () => { .getCNonceStateManager(issuer.context) .set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) - const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, userPinRequired: false } - - const result = await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest([openBadgeCredential.id], { - preAuthorizedCodeFlowConfig, + const result = await issuer.modules.openId4VcIssuer.createCredentialOffer({ + issuerId: openId4VcIssuer.issuerId, + offeredCredentials: [openBadgeCredential.id], + preAuthorizedCodeFlowConfig: { + preAuthorizedCode, + userPinRequired: false, + }, ...baseCredentialRequestOptions, }) - expect(result.credentialOfferRequest).toEqual( - 'openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%22%7D' + expect(result.credentialOfferUri).toEqual( + `openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` ) - const credential = new W3cCredential({ - type: universityDegreeCredential.types, - issuer: new W3cIssuer({ id: issuerDid }), - credentialSubject: new W3cCredentialSubject({ id: holderDid }), - issuanceDate: w3cDate(Date.now()), - }) - + const issuerMetadata = await issuer.modules.openId4VcIssuer.getIssuerMetadata(openId4VcIssuer.issuerId) await expect( - issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ - credential, - verificationMethod: issuerVerificationMethod, + issuer.modules.openId4VcIssuer.createCredentialResponse({ + issuerId: openId4VcIssuer.issuerId, + credentialRequest: await createCredentialRequest(holder.context, { - credentialSupported: openBadgeCredential, + credentialSupported: universityDegreeCredential, issuerMetadata, kid: holderKid, nonce: cNonce, }), + credentialRequestToCredentialMapper: () => { + throw new Error('Not implemented') + }, }) - ).rejects.toThrowError() + ).rejects.toThrowError('No offered credentials match the credential request.') }) it('pre authorized code flow using multiple credentials_supported', async () => { @@ -439,41 +472,50 @@ describe('OpenId4VcIssuer', () => { .getCNonceStateManager(issuer.context) .set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) - const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, userPinRequired: false } - - const result = await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest( - [openBadgeCredential.id, universityDegreeCredentialLd.id], - { - preAuthorizedCodeFlowConfig, - ...baseCredentialRequestOptions, - } - ) + const result = await issuer.modules.openId4VcIssuer.createCredentialOffer({ + offeredCredentials: [openBadgeCredential.id, universityDegreeCredentialLd.id], + issuerId: openId4VcIssuer.issuerId, + preAuthorizedCodeFlowConfig: { + preAuthorizedCode, + userPinRequired: false, + }, + ...baseCredentialRequestOptions, + }) - expect(result.credentialOfferRequest).toEqual( - 'openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%2C%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FUniversityDegreeCredentialLd%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%22%7D' + expect(result.credentialOfferUri).toEqual( + `openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%2C%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FUniversityDegreeCredentialLd%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` ) - const credential = new W3cCredential({ - type: universityDegreeCredentialLd.types, - issuer: new W3cIssuer({ id: issuerDid }), - credentialSubject: new W3cCredentialSubject({ id: holderDid }), - issuanceDate: w3cDate(Date.now()), - }) - - const issueCredentialResponse = await issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ - credential, - verificationMethod: issuerVerificationMethod, + const issuerMetadata = await issuer.modules.openId4VcIssuer.getIssuerMetadata(openId4VcIssuer.issuerId) + const issueCredentialResponse = await issuer.modules.openId4VcIssuer.createCredentialResponse({ + issuerId: openId4VcIssuer.issuerId, credentialRequest: await createCredentialRequest(holder.context, { credentialSupported: universityDegreeCredentialLd, issuerMetadata, kid: holderKid, nonce: cNonce, }), + credentialRequestToCredentialMapper: () => ({ + credential: new W3cCredential({ + type: universityDegreeCredentialLd.types, + issuer: new W3cIssuer({ id: issuerDid }), + credentialSubject: new W3cCredentialSubject({ id: holderDid }), + issuanceDate: w3cDate(Date.now()), + }), + verificationMethod: issuerVerificationMethod.id, + }), }) const sphereonW3cCredential = issueCredentialResponse.credential if (!sphereonW3cCredential) throw new Error('No credential found') + expect(issueCredentialResponse).toEqual({ + c_nonce: expect.any(String), + c_nonce_expires_in: 300000, + credential: expect.any(String), + format: 'jwt_vc_json-ld', + }) + await handleCredentialResponse(holder.context, sphereonW3cCredential, universityDegreeCredentialLd) }) @@ -486,31 +528,24 @@ describe('OpenId4VcIssuer', () => { .getCNonceStateManager(issuer.context) .set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) - const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { - preAuthorizedCode, - userPinRequired: false, - } - - const result = await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest([openBadgeCredential.id], { - preAuthorizedCodeFlowConfig, + const result = await issuer.modules.openId4VcIssuer.createCredentialOffer({ + offeredCredentials: [openBadgeCredential.id], + issuerId: openId4VcIssuer.issuerId, + preAuthorizedCodeFlowConfig: { + preAuthorizedCode, + userPinRequired: false, + }, ...baseCredentialRequestOptions, }) - expect(result.credentialOfferRequest).toEqual( - 'openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%22%7D' + expect(result.credentialOfferUri).toEqual( + `openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` ) - const credential = new W3cCredential({ - type: openBadgeCredential.types, - issuer: new W3cIssuer({ id: issuerDid }), - credentialSubject: new W3cCredentialSubject({ id: holderDid }), - issuanceDate: w3cDate(Date.now()), - }) - + const issuerMetadata = await issuer.modules.openId4VcIssuer.getIssuerMetadata(openId4VcIssuer.issuerId) await expect( - issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ - credential, - verificationMethod: issuerVerificationMethod, + issuer.modules.openId4VcIssuer.createCredentialResponse({ + issuerId: openId4VcIssuer.issuerId, credentialRequest: await createCredentialRequest(holder.context, { credentialSupported: { id: 'someid', @@ -521,8 +556,11 @@ describe('OpenId4VcIssuer', () => { kid: holderKid, nonce: cNonce, }), + credentialRequestToCredentialMapper: () => { + throw new Error('Not implemented') + }, }) - ).rejects.toThrowError() + ).rejects.toThrowError('No offered credentials match the credential request.') }) it('authorization code flow', async () => { @@ -534,27 +572,22 @@ describe('OpenId4VcIssuer', () => { .getCNonceStateManager(issuer.context) .set(cNonce, { cNonce: cNonce, createdAt: Date.now(), issuerState }) - const authorizationCodeFlowConfig: AuthorizationCodeFlowConfig = { issuerState } - - const result = await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest([openBadgeCredential.id], { - authorizationCodeFlowConfig, + const result = await issuer.modules.openId4VcIssuer.createCredentialOffer({ + offeredCredentials: [openBadgeCredential.id], + issuerId: openId4VcIssuer.issuerId, + authorizationCodeFlowConfig: { + issuerState, + }, ...baseCredentialRequestOptions, }) - expect(result.credentialOfferRequest).toEqual( - `openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%221234567890%22%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%22%7D` + expect(result.credentialOfferUri).toEqual( + `openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%221234567890%22%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` ) - const credential = new W3cCredential({ - type: ['VerifiableCredential', 'OpenBadgeCredential'], - issuer: new W3cIssuer({ id: issuerDid }), - credentialSubject: new W3cCredentialSubject({ id: holderDid }), - issuanceDate: w3cDate(Date.now()), - }) - - const issueCredentialResponse = await issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ - credential, - verificationMethod: issuerVerificationMethod, + const issuerMetadata = await issuer.modules.openId4VcIssuer.getIssuerMetadata(openId4VcIssuer.issuerId) + const issueCredentialResponse = await issuer.modules.openId4VcIssuer.createCredentialResponse({ + issuerId: openId4VcIssuer.issuerId, credentialRequest: await createCredentialRequest(holder.context, { credentialSupported: openBadgeCredential, issuerMetadata, @@ -562,56 +595,74 @@ describe('OpenId4VcIssuer', () => { nonce: cNonce, clientId: 'required', }), + credentialRequestToCredentialMapper: () => ({ + credential: new W3cCredential({ + type: ['VerifiableCredential', 'OpenBadgeCredential'], + issuer: new W3cIssuer({ id: issuerDid }), + credentialSubject: new W3cCredentialSubject({ id: holderDid }), + issuanceDate: w3cDate(Date.now()), + }), + verificationMethod: issuerVerificationMethod.id, + }), }) const sphereonW3cCredential = issueCredentialResponse.credential if (!sphereonW3cCredential) throw new Error('No credential found') + expect(issueCredentialResponse).toEqual({ + c_nonce: expect.any(String), + c_nonce_expires_in: 300000, + credential: expect.any(String), + format: 'jwt_vc_json', + }) + await handleCredentialResponse(holder.context, sphereonW3cCredential, openBadgeCredential) }) it('create credential offer and retrieve it from the uri (pre authorized flow)', async () => { const preAuthorizedCode = '1234567890' - const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, userPinRequired: false } - - const credentialOfferUri = 'https://openid4vc-issuer.com/credential-offer-uri' + const hostedCredentialOfferUri = 'https://openid4vc-issuer.com/credential-offer-uri' - const { credentialOfferRequest, credentialOfferPayload } = - await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest([openBadgeCredential.id], { - ...baseCredentialRequestOptions, - credentialOfferUri, - preAuthorizedCodeFlowConfig, - }) + const { credentialOfferUri, credentialOfferPayload } = await issuer.modules.openId4VcIssuer.createCredentialOffer({ + ...baseCredentialRequestOptions, + issuerId: openId4VcIssuer.issuerId, + offeredCredentials: [openBadgeCredential.id], + credentialOfferUri: hostedCredentialOfferUri, + preAuthorizedCodeFlowConfig: { + preAuthorizedCode, + userPinRequired: false, + }, + }) - expect(credentialOfferRequest).toEqual( - `openid-credential-offer://openid4vc-issuer.com?credential_offer_uri=${credentialOfferUri}` + expect(credentialOfferUri).toEqual( + `openid-credential-offer://openid4vc-issuer.com?credential_offer_uri=${hostedCredentialOfferUri}` ) const credentialOfferReceivedByUri = await issuer.modules.openId4VcIssuer.getCredentialOfferFromUri( - credentialOfferUri + hostedCredentialOfferUri ) expect(credentialOfferPayload).toEqual(credentialOfferReceivedByUri) }) it('create credential offer and retrieve it from the uri (authorizationCodeFlow)', async () => { - const authorizationCodeFlowConfig: AuthorizationCodeFlowConfig = { issuerState: '1234567890' } - const credentialOfferUri = 'https://openid4vc-issuer.com/credential-offer-uri' + const hostedCredentialOfferUri = 'https://openid4vc-issuer.com/credential-offer-uri' - const { credentialOfferRequest, credentialOfferPayload } = - await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest([openBadgeCredential.id], { - ...baseCredentialRequestOptions, - credentialOfferUri, - authorizationCodeFlowConfig, - }) + const { credentialOfferUri, credentialOfferPayload } = await issuer.modules.openId4VcIssuer.createCredentialOffer({ + offeredCredentials: [openBadgeCredential.id], + issuerId: openId4VcIssuer.issuerId, + ...baseCredentialRequestOptions, + credentialOfferUri: hostedCredentialOfferUri, + authorizationCodeFlowConfig: { issuerState: '1234567890' }, + }) - expect(credentialOfferRequest).toEqual( - `openid-credential-offer://openid4vc-issuer.com?credential_offer_uri=${credentialOfferUri}` + expect(credentialOfferUri).toEqual( + `openid-credential-offer://openid4vc-issuer.com?credential_offer_uri=${hostedCredentialOfferUri}` ) const credentialOfferReceivedByUri = await issuer.modules.openId4VcIssuer.getCredentialOfferFromUri( - credentialOfferUri + hostedCredentialOfferUri ) expect(credentialOfferPayload).toEqual(credentialOfferReceivedByUri) @@ -626,70 +677,80 @@ describe('OpenId4VcIssuer', () => { .getCNonceStateManager(issuer.context) .set(cNonce, { cNonce: cNonce, createdAt: Date.now(), preAuthorizedCode }) - const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { preAuthorizedCode, userPinRequired: false } - - const result = await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest( - [ - openBadgeCredential.id, - { - format: universityDegreeCredential.format, - types: universityDegreeCredential.types, - }, - ], - { - preAuthorizedCodeFlowConfig, - ...baseCredentialRequestOptions, - } - ) + const result = await issuer.modules.openId4VcIssuer.createCredentialOffer({ + offeredCredentials: [openBadgeCredential.id, universityDegreeCredential.id], + issuerId: openId4VcIssuer.issuerId, + preAuthorizedCodeFlowConfig: { + preAuthorizedCode, + userPinRequired: false, + }, + ...baseCredentialRequestOptions, + }) - expect(result.credentialOfferRequest).toEqual( - 'openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%2C%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22UniversityDegreeCredential%22%5D%7D%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%22%7D' + expect(result.credentialOfferUri).toEqual( + `openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%2C%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FUniversityDegreeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` ) - const credential = new W3cCredential({ - type: openBadgeCredential.types, - issuer: new W3cIssuer({ id: issuerDid }), - credentialSubject: new W3cCredentialSubject({ id: holderDid }), - issuanceDate: w3cDate(Date.now()), + const credentialRequestToCredentialMapper: OpenId4VciCredentialRequestToCredentialMapper = ({ + credentialsSupported, + }) => ({ + credential: new W3cCredential({ + type: + credentialsSupported[0].id === openBadgeCredential.id + ? openBadgeCredential.types + : universityDegreeCredential.types, + issuer: new W3cIssuer({ id: issuerDid }), + credentialSubject: new W3cCredentialSubject({ id: holderDid }), + issuanceDate: w3cDate(Date.now()), + }), + verificationMethod: issuerVerificationMethod.id, }) - const issueCredentialResponse = await issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ - credential, - verificationMethod: issuerVerificationMethod, + const issuerMetadata = await issuer.modules.openId4VcIssuer.getIssuerMetadata(openId4VcIssuer.issuerId) + const issueCredentialResponse = await issuer.modules.openId4VcIssuer.createCredentialResponse({ + issuerId: openId4VcIssuer.issuerId, credentialRequest: await createCredentialRequest(holder.context, { credentialSupported: openBadgeCredential, issuerMetadata, kid: holderKid, nonce: cNonce, }), + credentialRequestToCredentialMapper, }) const sphereonW3cCredential = issueCredentialResponse.credential if (!sphereonW3cCredential) throw new Error('No credential found') - await handleCredentialResponse(holder.context, sphereonW3cCredential, openBadgeCredential) - - const credential2 = new W3cCredential({ - type: universityDegreeCredential.types, - issuer: new W3cIssuer({ id: issuerDid }), - credentialSubject: new W3cCredentialSubject({ id: holderDid }), - issuanceDate: w3cDate(Date.now()), + expect(issueCredentialResponse).toEqual({ + c_nonce: expect.any(String), + c_nonce_expires_in: 300000, + credential: expect.any(String), + format: 'jwt_vc_json', }) - const issueCredentialResponse2 = await issuer.modules.openId4VcIssuer.createIssueCredentialResponse({ - credential: credential2, - verificationMethod: issuerVerificationMethod, + await handleCredentialResponse(holder.context, sphereonW3cCredential, openBadgeCredential) + + const issueCredentialResponse2 = await issuer.modules.openId4VcIssuer.createCredentialResponse({ + issuerId: openId4VcIssuer.issuerId, credentialRequest: await createCredentialRequest(holder.context, { credentialSupported: universityDegreeCredential, issuerMetadata, kid: holderKid, nonce: issueCredentialResponse.c_nonce ?? cNonce, }), + credentialRequestToCredentialMapper, }) const sphereonW3cCredential2 = issueCredentialResponse2.credential if (!sphereonW3cCredential2) throw new Error('No credential found') + expect(issueCredentialResponse2).toEqual({ + c_nonce: expect.any(String), + c_nonce_expires_in: 300000, + credential: expect.any(String), + format: 'jwt_vc_json', + }) + await handleCredentialResponse(holder.context, sphereonW3cCredential2, universityDegreeCredential) }) }) From 49d8a32a5cb19424c648a16087a9ace19880b805 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sun, 21 Jan 2024 21:24:09 +0700 Subject: [PATCH 103/115] rework some tests Signed-off-by: Timo Glastra --- .../dids/methods/web/WebDidResolver.ts | 7 + .../OpenId4VciHolderService.ts | 8 +- .../OpenId4VpHolderService.ts | 2 +- .../openid4vc-holder/__tests__/fixtures.ts | 758 +++++---------- .../__tests__/openId4vc-holder-module.test.ts | 4 +- .../__tests__/openid4vci-holder.e2e.test.ts | 861 ++++-------------- .../OpenId4VcVerifierApi.ts | 2 +- 7 files changed, 439 insertions(+), 1203 deletions(-) diff --git a/packages/core/src/modules/dids/methods/web/WebDidResolver.ts b/packages/core/src/modules/dids/methods/web/WebDidResolver.ts index 77d9b1e295..63b9976b2a 100644 --- a/packages/core/src/modules/dids/methods/web/WebDidResolver.ts +++ b/packages/core/src/modules/dids/methods/web/WebDidResolver.ts @@ -28,6 +28,13 @@ export class WebDidResolver implements DidResolver { const result = await this.resolver[parsed.method](did, parsed, this._resolverInstance, didResolutionOptions) let didDocument = null + + // If the did document uses the deprecated publicKey property + // we map it to the newer verificationMethod property + if (!result.didDocument?.verificationMethod && result.didDocument?.publicKey) { + result.didDocument.verificationMethod = result.didDocument.publicKey + } + if (result.didDocument) { didDocument = JsonTransformer.fromJSON(result.didDocument, DidDocument) } diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts index 0a6f5223c7..e1c767d814 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts @@ -343,7 +343,9 @@ export class OpenId4VciHolderService { } if (!accessTokenResponse.successBody) { - throw new AriesFrameworkError(`could not acquire access token from '${metadata.issuer}'.`) + throw new AriesFrameworkError( + `could not acquire access token from '${metadata.issuer}'. ${accessTokenResponse.errorBody?.error}: ${accessTokenResponse.errorBody?.error_description}` + ) } this.logger.debug('Requested OpenId4VCI Access Token.') @@ -572,7 +574,9 @@ export class OpenId4VciHolderService { this.logger.debug('Credential request response', credentialResponse) if (!credentialResponse.successBody || !credentialResponse.successBody.credential) { - throw new AriesFrameworkError('Did not receive a successful credential response.') + throw new AriesFrameworkError( + `Did not receive a successful credential response. ${credentialResponse.errorBody?.error}: ${credentialResponse.errorBody?.error_description}` + ) } const format = credentialResponse.successBody.format diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderService.ts index de4d42d333..6a08a400d2 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderService.ts @@ -25,7 +25,7 @@ import { VerificationMode, } from '@sphereon/did-auth-siop' -import { getResolver, getSuppliedSignatureFromVerificationMethod, getSupportedDidMethods } from '../../shared/utils' +import { getResolver, getSuppliedSignatureFromVerificationMethod, getSupportedDidMethods } from '../shared/utils' import { isVerifiedAuthorizationRequestWithPresentationDefinition, diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/fixtures.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/fixtures.ts index d5007b52ce..ea84c90eb3 100644 --- a/packages/openid4vc/src/openid4vc-holder/__tests__/fixtures.ts +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/fixtures.ts @@ -1,93 +1,8 @@ -const jsonLdPermanentResidentCardCredentialResponse = { - format: 'ldp_vc', - credential: { - type: ['VerifiableCredential', 'PermanentResidentCard'], - issuer: { - id: 'did:web:launchpad.vii.electron.mattrlabs.io', - name: 'Government of Kakapo', - logoUrl: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/logo.svg', - iconUrl: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/icon.svg', - image: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/icon.svg', - }, - name: 'Permanent Resident Card', - description: 'Government of Kakapo', - credentialBranding: { - backgroundColor: '#3a2d2d', - watermarkImageUrl: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/watermark@2x.png', - }, - issuanceDate: '2023-10-17T14:27:36.909Z', - credentialSubject: { - id: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', - type: ['PermanentResident', 'Person'], - image: - 'data:image/png;base64,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', - gender: 'Male', - birthDate: '1958-08-17', - givenName: 'Louis', - lprNumber: '1958-08-17', - familyName: 'Pasteur', - lprCategory: 'C09', - birthCountry: 'France', - residentSince: '2015-01-01', - commuterClassification: 'C1', - }, - '@context': [ - 'https://www.w3.org/2018/credentials/v1', - 'https://mattr.global/contexts/vc-extensions/v2', - 'https://w3id.org/citizenship/v1', - 'https://w3id.org/vc-revocation-list-2020/v1', - ], - credentialStatus: { - id: 'https://launchpad.vii.electron.mattrlabs.io/core/v1/revocation-lists/7bc7c021-56ee-445a-9143-fd79629df2aa#657', - type: 'RevocationList2020Status', - revocationListIndex: '657', - revocationListCredential: - 'https://launchpad.vii.electron.mattrlabs.io/core/v1/revocation-lists/7bc7c021-56ee-445a-9143-fd79629df2aa', - }, - proof: { - type: 'Ed25519Signature2018', - created: '2023-10-17T14:27:38Z', - jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..BaAHvbYzaFHp5rcqBChGVDd1gBpb9ezD4Rxn-Ev7uP1Jj71OfpcLH-oivuV90OGxgghaRwPe6rnBjwwo-RBjDg', - proofPurpose: 'assertionMethod', - verificationMethod: 'did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg', - }, - }, -} - -export const mattrLaunchpadJsonLd_draft_08 = { - wellKnownDid: { - id: 'did:web:launchpad.vii.electron.mattrlabs.io', - '@context': [ - 'https://www.w3.org/ns/did/v1', - 'https://w3id.org/security/suites/x25519-2019/v1', - 'https://w3id.org/security/suites/ed25519-2018/v1', - ], - keyAgreement: [ - { - id: 'did:web:launchpad.vii.electron.mattrlabs.io#9eS8Tqsus1', - type: 'X25519KeyAgreementKey2019', - controller: 'did:web:launchpad.vii.electron.mattrlabs.io', - publicKeyBase58: '9eS8Tqsus1uJmQpf37S8CnEeBrEehsC3qz8RMq67KoLB', - }, - ], - authentication: ['did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg'], - assertionMethod: ['did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg'], - capabilityDelegation: ['did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg'], - capabilityInvocation: ['did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg'], - verificationMethod: [ - { - id: 'did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg', - type: 'Ed25519VerificationKey2018', - controller: 'did:web:launchpad.vii.electron.mattrlabs.io', - publicKeyBase58: '6BhFMCGTJg9DnpXZe7zbiTrtuwion5FVV6Z2NUpwDMVT', - }, - ], - }, - credentialOfferAuthorizationCodeFlow: - 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=OpenBadgeCredential', - permanentResidentCardCredentialOffer: - 'openid-initiate-issuance://?issuer=https://launchpad.mattrlabs.com&credential_type=PermanentResidentCard&pre-authorized_code=krBcsBIlye2T-G4-rHHnRZUCah9uzDKwohJK6ABNvL-', +export const matrrLaunchpadDraft11JwtVcJson = { + credentialOffer: + 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Flaunchpad.vii.electron.mattrlabs.io%22%2C%22credentials%22%3A%5B%22613ecbbb-0a4c-4041-bb78-c64943139d5f%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22Jd6TUmLJct1DNyJpKKmt0i85scznBoJrEe_y_SlMW0j%22%7D%7D%7D', getMetadataResponse: { + issuer: 'https://launchpad.vii.electron.mattrlabs.io', authorization_endpoint: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/authorize', token_endpoint: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/token', jwks_uri: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/jwks', @@ -102,509 +17,326 @@ export const mattrLaunchpadJsonLd_draft_08 = { grant_types_supported: ['authorization_code', 'urn:ietf:params:oauth:grant-type:pre-authorized_code'], response_modes_supported: ['form_post', 'fragment', 'query'], response_types_supported: ['code id_token', 'code', 'id_token', 'none'], - scopes_supported: ['OpenBadgeCredential', 'AcademicAward', 'LearnerProfile', 'PermanentResidentCard'], + scopes_supported: ['OpenBadgeCredential', 'Passport'], token_endpoint_auth_signing_alg_values_supported: ['HS256', 'RS256', 'PS256', 'ES256', 'EdDSA'], credential_endpoint: 'https://launchpad.vii.electron.mattrlabs.io/oidc/v1/auth/credential', - credentials_supported: { - OpenBadgeCredential: { - formats: { - ldp_vc: { - name: 'JFF x vc-edu PlugFest 2', - description: "MATTR's submission for JFF Plugfest 2", - types: ['OpenBadgeCredential'], - binding_methods_supported: ['did'], - cryptographic_suites_supported: ['Ed25519Signature2018'], - }, - }, - }, - AcademicAward: { - formats: { - ldp_vc: { - name: 'Example Academic Award', - description: 'Microcredential from the MyCreds Network.', - types: ['AcademicAward'], - binding_methods_supported: ['did'], - cryptographic_suites_supported: ['Ed25519Signature2018'], + credentials_supported: [ + { + id: 'd2662472-891c-413d-b3c6-e2f0109001c5', + format: 'ldp_vc', + types: ['VerifiableCredential', 'OpenBadgeCredential'], + cryptographic_binding_methods_supported: ['did:key'], + cryptographic_suites_supported: ['Ed25519Signature2018'], + display: [ + { + name: 'Example University Degree', + description: 'JFF Plugfest 3 OpenBadge Credential', + background_color: '#464c49', + logo: {}, }, - }, + ], }, - LearnerProfile: { - formats: { - ldp_vc: { - name: 'Digitary Learner Profile', - description: 'Example', - types: ['LearnerProfile'], - binding_methods_supported: ['did'], - cryptographic_suites_supported: ['Ed25519Signature2018'], + { + id: 'b4c4cdf5-ccc9-4945-8c19-9334558653b2', + format: 'ldp_vc', + types: ['VerifiableCredential', 'Passport'], + cryptographic_binding_methods_supported: ['did:key'], + cryptographic_suites_supported: ['Ed25519Signature2018'], + display: [ + { + name: 'Passport', + description: 'Passport of the Kingdom of Kākāpō', + background_color: '#171717', + logo: { url: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/logo.svg' }, }, - }, + ], }, - PermanentResidentCard: { - formats: { - ldp_vc: { - name: 'Permanent Resident Card', - description: 'Government of Kakapo', - types: ['PermanentResidentCard'], - binding_methods_supported: ['did'], - cryptographic_suites_supported: ['Ed25519Signature2018'], + { + id: '613ecbbb-0a4c-4041-bb78-c64943139d5f', + format: 'jwt_vc_json', + types: ['VerifiableCredential', 'OpenBadgeCredential'], + cryptographic_binding_methods_supported: ['did:key'], + cryptographic_suites_supported: ['EdDSA'], + display: [ + { + name: 'Example University Degree', + description: 'JFF Plugfest 3 OpenBadge Credential', + background_color: '#464c49', + logo: {}, }, - }, - }, - }, - }, - - acquireAccessTokenResponse: { - access_token: '7nikUotMQefxn7oRX56R7MDNE7KJTGfwGjOkHzGaUIG', - expires_in: 3600, - scope: 'OpenBadgeCredential', - token_type: 'Bearer', - }, - credentialResponse: { - format: 'ldp_vc', - credential: { - type: ['VerifiableCredential', 'VerifiableCredentialExtension', 'OpenBadgeCredential'], - issuer: { - id: 'did:web:launchpad.vii.electron.mattrlabs.io', - name: 'Jobs for the Future (JFF)', - iconUrl: 'https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png', - image: 'https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/images/JFF_LogoLockup.png', - }, - name: 'JFF x vc-edu PlugFest 2', - description: "MATTR's submission for JFF Plugfest 2", - credentialBranding: { - backgroundColor: '#464c49', + ], }, - issuanceDate: '2023-01-25T16:58:06.292Z', - credentialSubject: { - id: 'did:key:z6MkpGR4gs4Rc3Zph4vj8wRnjnAxgAPSxcR8MAVKutWspQzc', - type: ['AchievementSubject'], - achievement: { - id: 'urn:uuid:bd6d9316-f7ae-4073-a1e5-2f7f5bd22922', - name: 'JFF x vc-edu PlugFest 2 Interoperability', - type: ['Achievement'], - image: { - id: 'https://w3c-ccg.github.io/vc-ed/plugfest-2-2022/images/JFF-VC-EDU-PLUGFEST2-badge-image.png', - type: 'Image', - }, - criteria: { - type: 'Criteria', - narrative: - 'Solutions providers earned this badge by demonstrating interoperability between multiple providers based on the OBv3 candidate final standard, with some additional required fields. Credential issuers earning this badge successfully issued a credential into at least two wallets. Wallet implementers earning this badge successfully displayed credentials issued by at least two different credential issuers.', + { + id: 'c3db5513-ae2b-46e9-8a0d-fbfd0ce52b6a', + format: 'jwt_vc_json', + types: ['VerifiableCredential', 'Passport'], + cryptographic_binding_methods_supported: ['did:key'], + cryptographic_suites_supported: ['EdDSA'], + display: [ + { + name: 'Passport', + description: 'Passport of the Kingdom of Kākāpō', + background_color: '#171717', + logo: { url: 'https://static.mattr.global/credential-assets/government-of-kakapo/web/logo.svg' }, }, - description: - 'This credential solution supports the use of OBv3 and w3c Verifiable Credentials and is interoperable with at least two other solutions. This was demonstrated successfully during JFF x vc-edu PlugFest 2.', - }, - }, - '@context': [ - 'https://www.w3.org/2018/credentials/v1', - { - '@vocab': 'https://w3id.org/security/undefinedTerm#', - }, - 'https://mattr.global/contexts/vc-extensions/v1', - 'https://purl.imsglobal.org/spec/ob/v3p0/context.json', - 'https://w3c-ccg.github.io/vc-status-rl-2020/contexts/vc-revocation-list-2020/v1.jsonld', - ], - credentialStatus: { - id: 'https://launchpad.vii.electron.mattrlabs.io/core/v1/revocation-lists/b4aa46a0-5539-4a6b-aa03-8f6791c22ce3#49', - type: 'RevocationList2020Status', - revocationListIndex: '49', - revocationListCredential: - 'https://launchpad.vii.electron.mattrlabs.io/core/v1/revocation-lists/b4aa46a0-5539-4a6b-aa03-8f6791c22ce3', - }, - proof: { - type: 'Ed25519Signature2018', - created: '2023-01-25T16:58:07Z', - jws: 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..PrpRKt60yXOzMNiQY5bELX40F6Svwm-FyQ-Jv02VJDfTTH8GPPByjtOb_n3YfWidQVgySfGQ_H7VmCGjvsU6Aw', - proofPurpose: 'assertionMethod', - verificationMethod: 'did:web:launchpad.vii.electron.mattrlabs.io#6BhFMCGTJg', + ], }, - }, + ], }, - jsonLdCredentialResponse: jsonLdPermanentResidentCardCredentialResponse, -} -export const waltIdJffJwt_draft_08 = { - credentialOffer: - 'openid-initiate-issuance://?issuer=https%3A%2F%2Fjff.walt.id%2Fissuer-api%2Fdefault%2Foidc%2F&credential_type=VerifiableId&pre-authorized_code=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI4YmI0NWZiNC0zNDc1LTQ5YzItODVjNy0wYjkxZjY4N2RhNDQiLCJwcmUtYXV0aG9yaXplZCI6dHJ1ZX0.R8nHseZJvU3uVL3Ox-97i1HUnvjZH6wKSWDO_i8D12I&user_pin_required=false', - getMetadataResponse: { - authorization_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/fulfillPAR', - token_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/token', - pushed_authorization_request_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/par', - issuer: 'https://jff.walt.id/issuer-api/default', - jwks_uri: 'https://jff.walt.id/issuer-api/default/oidc', - grant_types_supported: ['authorization_code', 'urn:ietf:params:oauth:grant-type:pre-authorized_code'], - request_uri_parameter_supported: true, - credentials_supported: { - VerifiableId: { - display: [{ name: 'VerifiableId' }], - formats: { - ldp_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableId'], - }, - jwt_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], - types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableId'], - }, - }, - }, - VerifiableDiploma: { - display: [{ name: 'VerifiableDiploma' }], - formats: { - ldp_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableDiploma'], - }, - jwt_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], - types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableDiploma'], - }, - }, - }, - VerifiableVaccinationCertificate: { - display: [{ name: 'VerifiableVaccinationCertificate' }], - formats: { - ldp_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableVaccinationCertificate'], - }, - jwt_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], - types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableVaccinationCertificate'], - }, - }, - }, - ProofOfResidence: { - display: [{ name: 'ProofOfResidence' }], - formats: { - ldp_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'VerifiableAttestation', 'ProofOfResidence'], - }, - jwt_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], - types: ['VerifiableCredential', 'VerifiableAttestation', 'ProofOfResidence'], - }, - }, - }, - ParticipantCredential: { - display: [{ name: 'ParticipantCredential' }], - formats: { - ldp_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'ParticipantCredential'], - }, - jwt_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], - types: ['VerifiableCredential', 'ParticipantCredential'], - }, - }, - }, - Europass: { - display: [{ name: 'Europass' }], - formats: { - ldp_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'VerifiableAttestation', 'Europass'], - }, - jwt_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], - types: ['VerifiableCredential', 'VerifiableAttestation', 'Europass'], - }, - }, + wellKnownDid: { + id: 'did:web:launchpad.vii.electron.mattrlabs.io', + '@context': 'https://w3.org/ns/did/v1', + // Uses deprecated publicKey, but the did:web resolver transforms + // it to the newer verificationMethod + publicKey: [ + { + id: 'did:web:launchpad.vii.electron.mattrlabs.io#Ck99k8Rd75', + type: 'Ed25519VerificationKey2018', + controller: 'did:web:launchpad.vii.electron.mattrlabs.io', + publicKeyBase58: 'Ck99k8Rd75V3THNexmMYYA6McqUJi9QgcPh4B1BBUTX7', }, - OpenBadgeCredential: { - display: [{ name: 'OpenBadgeCredential' }], - formats: { - ldp_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'OpenBadgeCredential'], - }, - jwt_vc: { - cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], - types: ['VerifiableCredential', 'OpenBadgeCredential'], - }, - }, + ], + keyAgreement: [ + { + id: 'did:web:launchpad.vii.electron.mattrlabs.io#Dd3FUiBvRy', + type: 'X25519KeyAgreementKey2019', + controller: 'did:web:launchpad.vii.electron.mattrlabs.io', + publicKeyBase58: 'Dd3FUiBvRyBcAbcywjGy99BtPaV2DXnvjbYPCu8MYs68', }, - }, - credential_issuer: { - display: [{ locale: null, name: 'https://jff.walt.id/issuer-api/default' }], - }, - credential_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/credential', - subject_types_supported: ['public'], + ], + authentication: ['did:web:launchpad.vii.electron.mattrlabs.io#Ck99k8Rd75'], + assertionMethod: ['did:web:launchpad.vii.electron.mattrlabs.io#Ck99k8Rd75'], + capabilityDelegation: ['did:web:launchpad.vii.electron.mattrlabs.io#Ck99k8Rd75'], + capabilityInvocation: ['did:web:launchpad.vii.electron.mattrlabs.io#Ck99k8Rd75'], }, acquireAccessTokenResponse: { - access_token: '8bb45fb4-3475-49c2-85c7-0b91f687da44', - refresh_token: 'WEjORX8NZccRGtRN4yvXFdYE8MeAOaLLmmGlcRbutq4', - c_nonce: 'cbad6376-f882-44c5-ae88-19bccc0de124', - id_token: - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI4YmI0NWZiNC0zNDc1LTQ5YzItODVjNy0wYjkxZjY4N2RhNDQifQ.Mca0Ln1AvNlxBJftYc1PZKQBlGdBmrHsFRQSBDoCgD0', + access_token: 'i3iOTQe5TOskOOUnkIDX29M8AuygT7Lfv3MkaHprL4p', + expires_in: 3600, + scope: 'OpenBadgeCredential', token_type: 'Bearer', - expires_in: 300, }, credentialResponse: { credential: - 'eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCMwIiwidHlwIjoiSldUIiwiYWxnIjoiRWREU0EifQ.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCIsInN1YiI6ImRpZDprZXk6ekRuYWVpcFdnOURNWFB0OWpjbUFCcWFZUlZLYzE5dFgxeGZCUldGc0pTUG9VZE1udiIsIm5iZiI6MTY4NTM1MDc4OSwiaWF0IjoxNjg1MzUwNzg5LCJ2YyI6eyJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVmVyaWZpYWJsZUF0dGVzdGF0aW9uIiwiVmVyaWZpYWJsZUlkIl0sIkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sImlkIjoidXJuOnV1aWQ6NTljZTRhYzItZWM2NS00YjhmLThmOTYtZWE3ODUxMmRmOWQzIiwiaXNzdWVyIjoiZGlkOmp3azpleUpyZEhraU9pSlBTMUFpTENKMWMyVWlPaUp6YVdjaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWEybGtJam9pTjJRMlkySm1NalE0T1dJek5ESTNObUl4TnpJeE9UQTFORGxrTWpNNU1UZ2lMQ0o0SWpvaVJtNUZWVlZoZFdSdE9UbE9NekJpT0RCcWN6aFdkRFJCYms5NGRsSjNXSFJuVW1OTGNUTm5Ra2wxT0NJc0ltRnNaeUk2SWtWa1JGTkJJbjAiLCJpc3N1YW5jZURhdGUiOiIyMDIzLTA1LTI5VDA4OjU5OjQ5WiIsImlzc3VlZCI6IjIwMjMtMDUtMjlUMDg6NTk6NDlaIiwidmFsaWRGcm9tIjoiMjAyMy0wNS0yOVQwODo1OTo0OVoiLCJjcmVkZW50aWFsU2NoZW1hIjp7ImlkIjoiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3dhbHQtaWQvd2FsdGlkLXNzaWtpdC12Y2xpYi9tYXN0ZXIvc3JjL3Rlc3QvcmVzb3VyY2VzL3NjaGVtYXMvVmVyaWZpYWJsZUlkLmpzb24iLCJ0eXBlIjoiRnVsbEpzb25TY2hlbWFWYWxpZGF0b3IyMDIxIn0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6RG5hZWlwV2c5RE1YUHQ5amNtQUJxYVlSVktjMTl0WDF4ZkJSV0ZzSlNQb1VkTW52IiwiY3VycmVudEFkZHJlc3MiOlsiMSBCb3VsZXZhcmQgZGUgbGEgTGliZXJ0w6ksIDU5ODAwIExpbGxlIl0sImRhdGVPZkJpcnRoIjoiMTk5My0wNC0wOCIsImZhbWlseU5hbWUiOiJET0UiLCJmaXJzdE5hbWUiOiJKYW5lIiwiZ2VuZGVyIjoiRkVNQUxFIiwibmFtZUFuZEZhbWlseU5hbWVBdEJpcnRoIjoiSmFuZSBET0UiLCJwZXJzb25hbElkZW50aWZpZXIiOiIwOTA0MDA4MDg0SCIsInBsYWNlT2ZCaXJ0aCI6IkxJTExFLCBGUkFOQ0UifSwiZXZpZGVuY2UiOlt7ImRvY3VtZW50UHJlc2VuY2UiOlsiUGh5c2ljYWwiXSwiZXZpZGVuY2VEb2N1bWVudCI6WyJQYXNzcG9ydCJdLCJzdWJqZWN0UHJlc2VuY2UiOiJQaHlzaWNhbCIsInR5cGUiOlsiRG9jdW1lbnRWZXJpZmljYXRpb24iXSwidmVyaWZpZXIiOiJkaWQ6ZWJzaToyQTlCWjlTVWU2QmF0YWNTcHZzMVY1Q2RqSHZMcFE3YkVzaTJKYjZMZEhLblF4YU4ifV19LCJqdGkiOiJ1cm46dXVpZDo1OWNlNGFjMi1lYzY1LTRiOGYtOGY5Ni1lYTc4NTEyZGY5ZDMifQ.6Wn8X2tEQJ9CmX3-meCxDuGmevRdtivnjVkGPXzfnJ-1M6AU4SFxxon0JmMjdmO_h4P9sCEe9RTtyTJou2yeCA', - format: 'jwt_vc', + 'eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bGF1bmNocGFkLnZpaS5lbGVjdHJvbi5tYXR0cmxhYnMuaW8jQ2s5OWs4UmQ3NSJ9.eyJpc3MiOiJkaWQ6d2ViOmxhdW5jaHBhZC52aWkuZWxlY3Ryb24ubWF0dHJsYWJzLmlvIiwic3ViIjoiZGlkOmtleTp6Nk1rcEdSNGdzNFJjM1pwaDR2ajh3Um5qbkF4Z0FQU3hjUjhNQVZLdXRXc3BRemMiLCJuYmYiOjE3MDU4NDAzMDksImV4cCI6MTczNzQ2MjcwOSwidmMiOnsibmFtZSI6IkV4YW1wbGUgVW5pdmVyc2l0eSBEZWdyZWUiLCJkZXNjcmlwdGlvbiI6IkpGRiBQbHVnZmVzdCAzIE9wZW5CYWRnZSBDcmVkZW50aWFsIiwiY3JlZGVudGlhbEJyYW5kaW5nIjp7ImJhY2tncm91bmRDb2xvciI6IiM0NjRjNDkifSwiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL21hdHRyLmdsb2JhbC9jb250ZXh0cy92Yy1leHRlbnNpb25zL3YyIiwiaHR0cHM6Ly9wdXJsLmltc2dsb2JhbC5vcmcvc3BlYy9vYi92M3AwL2NvbnRleHQtMy4wLjIuanNvbiIsImh0dHBzOi8vcHVybC5pbXNnbG9iYWwub3JnL3NwZWMvb2IvdjNwMC9leHRlbnNpb25zLmpzb24iLCJodHRwczovL3czaWQub3JnL3ZjLXJldm9jYXRpb24tbGlzdC0yMDIwL3YxIiwiaHR0cHM6Ly93M2lkLm9yZy92Yy1yZXZvY2F0aW9uLWxpc3QtMjAyMC92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiT3BlbkJhZGdlQ3JlZGVudGlhbCJdLCJjcmVkZW50aWFsU3ViamVjdCI6eyJpZCI6ImRpZDprZXk6ejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjIiwidHlwZSI6WyJBY2hpZXZlbWVudFN1YmplY3QiXSwiYWNoaWV2ZW1lbnQiOnsiaWQiOiJodHRwczovL2V4YW1wbGUuY29tL2FjaGlldmVtZW50cy8yMXN0LWNlbnR1cnktc2tpbGxzL3RlYW13b3JrIiwibmFtZSI6IlRlYW13b3JrIiwidHlwZSI6WyJBY2hpZXZlbWVudCJdLCJpbWFnZSI6eyJpZCI6Imh0dHBzOi8vdzNjLWNjZy5naXRodWIuaW8vdmMtZWQvcGx1Z2Zlc3QtMy0yMDIzL2ltYWdlcy9KRkYtVkMtRURVLVBMVUdGRVNUMy1iYWRnZS1pbWFnZS5wbmciLCJ0eXBlIjoiSW1hZ2UifSwiY3JpdGVyaWEiOnsibmFycmF0aXZlIjoiVGVhbSBtZW1iZXJzIGFyZSBub21pbmF0ZWQgZm9yIHRoaXMgYmFkZ2UgYnkgdGhlaXIgcGVlcnMgYW5kIHJlY29nbml6ZWQgdXBvbiByZXZpZXcgYnkgRXhhbXBsZSBDb3JwIG1hbmFnZW1lbnQuIn0sImRlc2NyaXB0aW9uIjoiVGhpcyBiYWRnZSByZWNvZ25pemVzIHRoZSBkZXZlbG9wbWVudCBvZiB0aGUgY2FwYWNpdHkgdG8gY29sbGFib3JhdGUgd2l0aGluIGEgZ3JvdXAgZW52aXJvbm1lbnQuIn19LCJpc3N1ZXIiOnsiaWQiOiJkaWQ6d2ViOmxhdW5jaHBhZC52aWkuZWxlY3Ryb24ubWF0dHJsYWJzLmlvIiwibmFtZSI6IkV4YW1wbGUgVW5pdmVyc2l0eSIsImljb25VcmwiOiJodHRwczovL3czYy1jY2cuZ2l0aHViLmlvL3ZjLWVkL3BsdWdmZXN0LTEtMjAyMi9pbWFnZXMvSkZGX0xvZ29Mb2NrdXAucG5nIiwiaW1hZ2UiOiJodHRwczovL3czYy1jY2cuZ2l0aHViLmlvL3ZjLWVkL3BsdWdmZXN0LTEtMjAyMi9pbWFnZXMvSkZGX0xvZ29Mb2NrdXAucG5nIn19fQ.u33C1y8qwlKQSIq5NjgjXq-fG_u5-bP87HAZPiaTtXhUzd5hxToyrEUb3GAEa4dkLY2TVQA1LtC6sNSUmGevBQ', + format: 'jwt_vc_json', }, } -// This object is MANUALLY converted and should be updated when we have actual test vectors -export const waltIdJffJwt_draft_11 = { +export const waltIdDraft11JwtVcJson = { credentialOffer: - 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fjff.walt.id%2Fissuer-api%2Fdefault%2Foidc%22%2C%22credentials%22%3A%5B%22VerifiableId%22%2C%20%22VerifiableDiploma%22%5D%2C%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22ABC%22%7D%7D%7D', + 'openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.portal.walt.id%22%2C%22credentials%22%3A%5B%22UniversityDegree%22%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22efc2f5dd-0f44-4f38-a902-3204e732c391%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJlZmMyZjVkZC0wZjQ0LTRmMzgtYTkwMi0zMjA0ZTczMmMzOTEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.OHzYTP_u6I95hHBmjF3RchydGidq3nsT0QHdgJ1AXyR5AFkrTfJwsW4FQIdOdda93uS7FOh_vSVGY0Qngzm7Ag%22%2C%22user_pin_required%22%3Afalse%7D%7D%7D', getMetadataResponse: { - authorization_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/fulfillPAR', - token_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/token', - pushed_authorization_request_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/par', - credential_issuer: 'https://jff.walt.id/issuer-api/default', - jwks_uri: 'https://jff.walt.id/issuer-api/default/oidc', - credential_endpoint: 'https://jff.walt.id/issuer-api/default/oidc/credential', - subject_types_supported: ['public'], + issuer: 'https://issuer.portal.walt.id', + authorization_endpoint: 'https://issuer.portal.walt.id/authorize', + pushed_authorization_request_endpoint: 'https://issuer.portal.walt.id/par', + token_endpoint: 'https://issuer.portal.walt.id/token', + jwks_uri: 'https://issuer.portal.walt.id/jwks', + scopes_supported: ['openid'], + response_modes_supported: ['query', 'fragment'], grant_types_supported: ['authorization_code', 'urn:ietf:params:oauth:grant-type:pre-authorized_code'], - request_uri_parameter_supported: true, + subject_types_supported: ['public'], + credential_issuer: 'https://issuer.portal.walt.id/.well-known/openid-credential-issuer', + credential_endpoint: 'https://issuer.portal.walt.id/credential', credentials_supported: [ { - id: 'VerifiableId', format: 'jwt_vc_json', + id: 'BankId', cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: ['ES256', 'ES256K', 'EdDSA', 'RS256', 'PS256'], - types: ['VerifiableCredential', 'VerifiableId'], + cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + types: ['VerifiableCredential', 'BankId'], }, { - id: 'VerifiableDiploma', - display: [{ name: 'VerifiableDiploma' }], - format: 'ldp_vc', + format: 'jwt_vc_json', + id: 'KycChecksCredential', cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableDiploma'], + cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + types: ['VerifiableCredential', 'VerifiableAttestation', 'KycChecksCredential'], }, { - id: 'VerifiableVaccinationCertificate', - display: [{ name: 'VerifiableVaccinationCertificate' }], - format: 'ldp_vc', + format: 'jwt_vc_json', + id: 'KycDataCredential', cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableVaccinationCertificate'], + cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + types: ['VerifiableCredential', 'VerifiableAttestation', 'KycDataCredential'], }, { - id: 'ProofOfResidence', - display: [{ name: 'ProofOfResidence' }], - format: 'ldp_vc', + format: 'jwt_vc_json', + id: 'PassportCh', cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'VerifiableAttestation', 'ProofOfResidence'], + cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableId', 'PassportCh'], }, { - id: 'ParticipantCredential', - format: 'ldp_vc', - display: [{ name: 'ParticipantCredential' }], + format: 'jwt_vc_json', + id: 'PND91Credential', cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'ParticipantCredential'], + cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + types: ['VerifiableCredential', 'PND91Credential'], }, { - id: 'Europass', - display: [{ name: 'Europass' }], - format: 'ldp_vc', + format: 'jwt_vc_json', + id: 'MortgageEligibility', cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], - types: ['VerifiableCredential', 'VerifiableAttestation', 'Europass'], + cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableId', 'MortgageEligibility'], }, { + format: 'jwt_vc_json', + id: 'PortableDocumentA1', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + types: ['VerifiableCredential', 'VerifiableAttestation', 'PortableDocumentA1'], + }, + { + format: 'jwt_vc_json', id: 'OpenBadgeCredential', - display: [{ name: 'OpenBadgeCredential' }], - format: 'ldp_vc', cryptographic_binding_methods_supported: ['did'], - cryptographic_suites_supported: [ - 'Ed25519Signature2018', - 'Ed25519Signature2020', - 'EcdsaSecp256k1Signature2019', - 'RsaSignature2018', - 'JsonWebSignature2020', - 'JcsEd25519Signature2020', - ], + cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], types: ['VerifiableCredential', 'OpenBadgeCredential'], }, - ], - }, - - acquireAccessTokenResponse: { - access_token: '8bb45fb4-3475-49c2-85c7-0b91f687da44', - refresh_token: 'WEjORX8NZccRGtRN4yvXFdYE8MeAOaLLmmGlcRbutq4', - c_nonce: 'cbad6376-f882-44c5-ae88-19bccc0de124', - id_token: - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI4YmI0NWZiNC0zNDc1LTQ5YzItODVjNy0wYjkxZjY4N2RhNDQifQ.Mca0Ln1AvNlxBJftYc1PZKQBlGdBmrHsFRQSBDoCgD0', - token_type: 'Bearer', - expires_in: 300, - }, - - credentialResponse: { - credential: - 'eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCMwIiwidHlwIjoiSldUIiwiYWxnIjoiRWREU0EifQ.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCIsInN1YiI6ImRpZDprZXk6ekRuYWVpcFdnOURNWFB0OWpjbUFCcWFZUlZLYzE5dFgxeGZCUldGc0pTUG9VZE1udiIsIm5iZiI6MTY4NTM1MDc4OSwiaWF0IjoxNjg1MzUwNzg5LCJ2YyI6eyJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVmVyaWZpYWJsZUF0dGVzdGF0aW9uIiwiVmVyaWZpYWJsZUlkIl0sIkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sImlkIjoidXJuOnV1aWQ6NTljZTRhYzItZWM2NS00YjhmLThmOTYtZWE3ODUxMmRmOWQzIiwiaXNzdWVyIjoiZGlkOmp3azpleUpyZEhraU9pSlBTMUFpTENKMWMyVWlPaUp6YVdjaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWEybGtJam9pTjJRMlkySm1NalE0T1dJek5ESTNObUl4TnpJeE9UQTFORGxrTWpNNU1UZ2lMQ0o0SWpvaVJtNUZWVlZoZFdSdE9UbE9NekJpT0RCcWN6aFdkRFJCYms5NGRsSjNXSFJuVW1OTGNUTm5Ra2wxT0NJc0ltRnNaeUk2SWtWa1JGTkJJbjAiLCJpc3N1YW5jZURhdGUiOiIyMDIzLTA1LTI5VDA4OjU5OjQ5WiIsImlzc3VlZCI6IjIwMjMtMDUtMjlUMDg6NTk6NDlaIiwidmFsaWRGcm9tIjoiMjAyMy0wNS0yOVQwODo1OTo0OVoiLCJjcmVkZW50aWFsU2NoZW1hIjp7ImlkIjoiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3dhbHQtaWQvd2FsdGlkLXNzaWtpdC12Y2xpYi9tYXN0ZXIvc3JjL3Rlc3QvcmVzb3VyY2VzL3NjaGVtYXMvVmVyaWZpYWJsZUlkLmpzb24iLCJ0eXBlIjoiRnVsbEpzb25TY2hlbWFWYWxpZGF0b3IyMDIxIn0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6RG5hZWlwV2c5RE1YUHQ5amNtQUJxYVlSVktjMTl0WDF4ZkJSV0ZzSlNQb1VkTW52IiwiY3VycmVudEFkZHJlc3MiOlsiMSBCb3VsZXZhcmQgZGUgbGEgTGliZXJ0w6ksIDU5ODAwIExpbGxlIl0sImRhdGVPZkJpcnRoIjoiMTk5My0wNC0wOCIsImZhbWlseU5hbWUiOiJET0UiLCJmaXJzdE5hbWUiOiJKYW5lIiwiZ2VuZGVyIjoiRkVNQUxFIiwibmFtZUFuZEZhbWlseU5hbWVBdEJpcnRoIjoiSmFuZSBET0UiLCJwZXJzb25hbElkZW50aWZpZXIiOiIwOTA0MDA4MDg0SCIsInBsYWNlT2ZCaXJ0aCI6IkxJTExFLCBGUkFOQ0UifSwiZXZpZGVuY2UiOlt7ImRvY3VtZW50UHJlc2VuY2UiOlsiUGh5c2ljYWwiXSwiZXZpZGVuY2VEb2N1bWVudCI6WyJQYXNzcG9ydCJdLCJzdWJqZWN0UHJlc2VuY2UiOiJQaHlzaWNhbCIsInR5cGUiOlsiRG9jdW1lbnRWZXJpZmljYXRpb24iXSwidmVyaWZpZXIiOiJkaWQ6ZWJzaToyQTlCWjlTVWU2QmF0YWNTcHZzMVY1Q2RqSHZMcFE3YkVzaTJKYjZMZEhLblF4YU4ifV19LCJqdGkiOiJ1cm46dXVpZDo1OWNlNGFjMi1lYzY1LTRiOGYtOGY5Ni1lYTc4NTEyZGY5ZDMifQ.6Wn8X2tEQJ9CmX3-meCxDuGmevRdtivnjVkGPXzfnJ-1M6AU4SFxxon0JmMjdmO_h4P9sCEe9RTtyTJou2yeCA', - format: 'jwt_vc', - }, - - jsonLdCredentialResponse: jsonLdPermanentResidentCardCredentialResponse, -} - -export const waltIssuerPortalV11 = { - issuerMetadata: { - issuer: 'https://issuer.portal.walt.id', - authorization_endpoint: 'https://issuer.portal.walt.id/authorize', - pushed_authorization_request_endpoint: 'https://issuer.portal.walt.id/par', - token_endpoint: 'https://issuer.portal.walt.id/token', - jwks_uri: 'https://issuer.portal.walt.id/jwks', - scopes_supported: ['openid'], - response_modes_supported: ['query', 'fragment'], - grant_types_supported: ['authorization_code', 'urn:ietf:params:oauth:grant-type:pre-authorized_code'], - subject_types_supported: ['public'], - credential_issuer: 'https://issuer.portal.walt.id/.well-known/openid-credential-issuer', - credential_endpoint: 'https://issuer.portal.walt.id/credential', - credentials_supported: [ { format: 'jwt_vc_json', - id: 'VerifiableId', + id: 'VaccinationCertificate', cryptographic_binding_methods_supported: ['did'], cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], - types: ['VerifiableCredential', 'VerifiableId'], + types: ['VerifiableCredential', 'VerifiableAttestation', 'VaccinationCertificate'], }, { format: 'jwt_vc_json', - id: 'VerifiableDiploma', + id: 'WalletHolderCredential', cryptographic_binding_methods_supported: ['did'], cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], - types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableDiploma'], + types: ['VerifiableCredential', 'WalletHolderCredential'], }, { format: 'jwt_vc_json', - id: 'OpenBadgeCredential', + id: 'UniversityDegree', cryptographic_binding_methods_supported: ['did'], cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], - types: ['VerifiableCredential', 'OpenBadgeCredential'], + types: ['VerifiableCredential', 'UniversityDegree'], + }, + { + format: 'jwt_vc_json', + id: 'VerifiableId', + cryptographic_binding_methods_supported: ['did'], + cryptographic_suites_supported: ['EdDSA', 'ES256', 'ES256K', 'RSA'], + types: ['VerifiableCredential', 'VerifiableAttestation', 'VerifiableId'], }, ], batch_credential_endpoint: 'https://issuer.portal.walt.id/batch_credential', deferred_credential_endpoint: 'https://issuer.portal.walt.id/credential_deferred', }, + + acquireAccessTokenResponse: { + access_token: + 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJjMDQyMmUxMy1kNTU0LTQwMmUtOTQ0OS0yZjA0ZjAyNjMzNTMiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IkFDQ0VTUyJ9.pkNF05uUy72QAoZwdf1Uz1XRc4aGs1hhnim-x1qIeMe17TMUYV2D6BOATQtDItxnnhQz2MBfqUSQKYi7CFirDA', + token_type: 'bearer', + c_nonce: 'd4364dac-f026-4380-a4c3-2bfe2d2df52a', + c_nonce_expires_in: 27, + }, + + authorizationCode: + 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJkZDYyOGQxYy1kYzg4LTQ2OGItYjI5Yi05ODQwMzFlNzg3OWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.86LfW1y7QwNObIhJej40E4Ea8PGjBbIeq1KBkOWOLNnOs5rRvtDkazA52npsKrBKqfoqCPmOHcVAvPZPWJhKAA', + par: { - request_uri: 'urn:ietf:params:oauth:request_uri:b0e16785-d722-42a5-a04f-4beab28e03ea', - expires_in: 'PT4M0.516515278S', + request_uri: 'urn:ietf:params:oauth:request_uri:738f2ac2-18ac-4162-b0a8-5e0e6ba2270b', + expires_in: 'PT3M46.132011234S', + }, + + credentialResponse: { + credential: + 'eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDpqd2s6ZXlKcmRIa2lPaUpQUzFBaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWEybGtJam9pUTBaUkxVNXlZVFY1Ym5sQ2MyWjRkM2szWVU1bU9HUjFRVVZWUTAxc1RVbHlVa2x5UkdjMlJFbDVOQ0lzSW5naU9pSm9OVzVpZHpaWU9VcHRTVEJDZG5WUk5VMHdTbGhtZWs4NGN6SmxSV0pRWkZZeU9YZHpTRlJNT1hCckluMCJ9.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0pqY25ZaU9pSkZaREkxTlRFNUlpd2lhMmxrSWpvaVEwWlJMVTV5WVRWNWJubENjMlo0ZDNrM1lVNW1PR1IxUVVWVlEwMXNUVWx5VWtseVJHYzJSRWw1TkNJc0luZ2lPaUpvTlc1aWR6WllPVXB0U1RCQ2RuVlJOVTB3U2xobWVrODRjekpsUldKUVpGWXlPWGR6U0ZSTU9YQnJJbjAiLCJzdWIiOiJkaWQ6a2V5Ono2TWtwR1I0Z3M0UmMzWnBoNHZqOHdSbmpuQXhnQVBTeGNSOE1BVkt1dFdzcFF6YyN6Nk1rcEdSNGdzNFJjM1pwaDR2ajh3Um5qbkF4Z0FQU3hjUjhNQVZLdXRXc3BRemMiLCJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sImlkIjoidXJuOnV1aWQ6NmU2ODVlOGUtNmRmNS00NzhkLTlkNWQtNDk2ZTcxMDJkYmFhIiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlVuaXZlcnNpdHlEZWdyZWUiXSwiaXNzdWVyIjp7ImlkIjoiZGlkOmp3azpleUpyZEhraU9pSlBTMUFpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lRMFpSTFU1eVlUVjVibmxDYzJaNGQzazNZVTVtT0dSMVFVVlZRMDFzVFVseVVrbHlSR2MyUkVsNU5DSXNJbmdpT2lKb05XNWlkelpZT1VwdFNUQkNkblZSTlUwd1NsaG1lazg0Y3pKbFJXSlFaRll5T1hkelNGUk1PWEJySW4wIn0sImlzc3VhbmNlRGF0ZSI6IjIwMjQtMDEtMjFUMTI6NDU6NDYuOTU1MjU0MDg3WiIsImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rcEdSNGdzNFJjM1pwaDR2ajh3Um5qbkF4Z0FQU3hjUjhNQVZLdXRXc3BRemMjejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjIiwiZGVncmVlIjp7InR5cGUiOiJCYWNoZWxvckRlZ3JlZSIsIm5hbWUiOiJCYWNoZWxvciBvZiBTY2llbmNlIGFuZCBBcnRzIn19fSwianRpIjoidXJuOnV1aWQ6NmU2ODVlOGUtNmRmNS00NzhkLTlkNWQtNDk2ZTcxMDJkYmFhIiwiaWF0IjoxNzA1ODQxMTQ2LCJuYmYiOjE3MDU4NDEwNTZ9.sEudi9lL4YSvMdfjRaeDoRl2_p6dpfuxw_qkPXeBx8FRIQ41t-fyH_S_CDTVYH7wwL-RDbVMK1cza2FQH65hCg', + format: 'jwt_vc_json', + }, +} + +export const animoOpenIdPlaygroundDraft11SdJwtVc = { + credentialOffer: + 'openid-credential-offer://?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221076398228999891821960009%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22AnimoOpenId4VcPlaygroundSdJwtVcJwk%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc.animo.id%2Foid4vci%2F0bbfb1c0-9f45-478c-a139-08f6ed610a37%22%7D', + getMetadataResponse: { + credential_issuer: 'https://openid4vc.animo.id/oid4vci/0bbfb1c0-9f45-478c-a139-08f6ed610a37', + token_endpoint: 'https://openid4vc.animo.id/oid4vci/0bbfb1c0-9f45-478c-a139-08f6ed610a37/token', + credential_endpoint: 'https://openid4vc.animo.id/oid4vci/0bbfb1c0-9f45-478c-a139-08f6ed610a37/credential', + credentials_supported: [ + { + id: 'AnimoOpenId4VcPlaygroundSdJwtVcDid', + format: 'vc+sd-jwt', + vct: 'AnimoOpenId4VcPlayground', + cryptographic_binding_methods_supported: ['did:key', 'did:jwk'], + cryptographic_suites_supported: ['EdDSA'], + display: [ + { + name: 'Animo OpenID4VC Playground - SD-JWT-VC (did holder binding)', + description: "Issued using Animo's OpenID4VC Playground", + background_color: '#FFFFFF', + locale: 'en', + text_color: '#E17471', + }, + ], + }, + { + id: 'AnimoOpenId4VcPlaygroundSdJwtVcJwk', + format: 'vc+sd-jwt', + vct: 'AnimoOpenId4VcPlayground', + cryptographic_binding_methods_supported: ['jwk'], + cryptographic_suites_supported: ['EdDSA'], + display: [ + { + name: 'Animo OpenID4VC Playground - SD-JWT-VC (jwk holder binding)', + description: "Issued using Animo's OpenID4VC Playground", + background_color: '#FFFFFF', + locale: 'en', + text_color: '#E17471', + }, + ], + }, + { + id: 'AnimoOpenId4VcPlaygroundJwtVc', + format: 'jwt_vc_json', + types: ['AnimoOpenId4VcPlayground'], + cryptographic_binding_methods_supported: ['did:key', 'did:jwk'], + cryptographic_suites_supported: ['EdDSA'], + display: [ + { + name: 'Animo OpenID4VC Playground - JWT VC', + description: "Issued using Animo's OpenID4VC Playground", + background_color: '#FFFFFF', + locale: 'en', + text_color: '#E17471', + }, + ], + }, + ], + display: [ + { + background_color: '#FFFFFF', + description: 'Animo OpenID4VC Playground', + name: 'Animo OpenID4VC Playground', + locale: 'en', + logo: { alt_text: 'Animo logo', url: 'https://i.imgur.com/8B37E4a.png' }, + text_color: '#E17471', + }, + ], }, acquireAccessTokenResponse: { - access_token: '8bb45fb4-3475-49c2-85c7-0b91f687da44', - refresh_token: 'WEjORX8NZccRGtRN4yvXFdYE8MeAOaLLmmGlcRbutq4', - c_nonce: 'cbad6376-f882-44c5-ae88-19bccc0de124', - id_token: - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI4YmI0NWZiNC0zNDc1LTQ5YzItODVjNy0wYjkxZjY4N2RhNDQifQ.Mca0Ln1AvNlxBJftYc1PZKQBlGdBmrHsFRQSBDoCgD0', - token_type: 'Bearer', - expires_in: 300, + access_token: + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6Im5fQ05IM3c1dWpQaDNsTmVaR05Ta0hiT2pSTnNudkJpNXIzcXhINGZwd1UifX0.eyJpc3MiOiJodHRwczovL29wZW5pZDR2Yy5hbmltby5pZC9vaWQ0dmNpLzBiYmZiMWMwLTlmNDUtNDc4Yy1hMTM5LTA4ZjZlZDYxMGEzNyIsImV4cCI6MTgwMDAwLCJpYXQiOjE3MDU4NDM1NzM1ODh9.3JC_R4zXK0GLMG6MS7ClVWm9bK-9v7mA2iS_0hqYdmZRwXJI3ME6TAslPZNNdxCTp5ZYzzsFuLd2L3l7kULmBQ', + token_type: 'bearer', + expires_in: 180000, + c_nonce: '725150697872293881791236', + c_nonce_expires_in: 300000, + authorization_pending: false, }, credentialResponse: { credential: - 'eyJraWQiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCMwIiwidHlwIjoiSldUIiwiYWxnIjoiRWREU0EifQ.eyJpc3MiOiJkaWQ6andrOmV5SnJkSGtpT2lKUFMxQWlMQ0oxYzJVaU9pSnphV2NpTENKamNuWWlPaUpGWkRJMU5URTVJaXdpYTJsa0lqb2lOMlEyWTJKbU1qUTRPV0l6TkRJM05tSXhOekl4T1RBMU5EbGtNak01TVRnaUxDSjRJam9pUm01RlZWVmhkV1J0T1RsT016QmlPREJxY3poV2REUkJiazk0ZGxKM1dIUm5VbU5MY1ROblFrbDFPQ0lzSW1Gc1p5STZJa1ZrUkZOQkluMCIsInN1YiI6ImRpZDprZXk6ekRuYWVpcFdnOURNWFB0OWpjbUFCcWFZUlZLYzE5dFgxeGZCUldGc0pTUG9VZE1udiIsIm5iZiI6MTY4NTM1MDc4OSwiaWF0IjoxNjg1MzUwNzg5LCJ2YyI6eyJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVmVyaWZpYWJsZUF0dGVzdGF0aW9uIiwiVmVyaWZpYWJsZUlkIl0sIkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIl0sImlkIjoidXJuOnV1aWQ6NTljZTRhYzItZWM2NS00YjhmLThmOTYtZWE3ODUxMmRmOWQzIiwiaXNzdWVyIjoiZGlkOmp3azpleUpyZEhraU9pSlBTMUFpTENKMWMyVWlPaUp6YVdjaUxDSmpjbllpT2lKRlpESTFOVEU1SWl3aWEybGtJam9pTjJRMlkySm1NalE0T1dJek5ESTNObUl4TnpJeE9UQTFORGxrTWpNNU1UZ2lMQ0o0SWpvaVJtNUZWVlZoZFdSdE9UbE9NekJpT0RCcWN6aFdkRFJCYms5NGRsSjNXSFJuVW1OTGNUTm5Ra2wxT0NJc0ltRnNaeUk2SWtWa1JGTkJJbjAiLCJpc3N1YW5jZURhdGUiOiIyMDIzLTA1LTI5VDA4OjU5OjQ5WiIsImlzc3VlZCI6IjIwMjMtMDUtMjlUMDg6NTk6NDlaIiwidmFsaWRGcm9tIjoiMjAyMy0wNS0yOVQwODo1OTo0OVoiLCJjcmVkZW50aWFsU2NoZW1hIjp7ImlkIjoiaHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL3dhbHQtaWQvd2FsdGlkLXNzaWtpdC12Y2xpYi9tYXN0ZXIvc3JjL3Rlc3QvcmVzb3VyY2VzL3NjaGVtYXMvVmVyaWZpYWJsZUlkLmpzb24iLCJ0eXBlIjoiRnVsbEpzb25TY2hlbWFWYWxpZGF0b3IyMDIxIn0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6RG5hZWlwV2c5RE1YUHQ5amNtQUJxYVlSVktjMTl0WDF4ZkJSV0ZzSlNQb1VkTW52IiwiY3VycmVudEFkZHJlc3MiOlsiMSBCb3VsZXZhcmQgZGUgbGEgTGliZXJ0w6ksIDU5ODAwIExpbGxlIl0sImRhdGVPZkJpcnRoIjoiMTk5My0wNC0wOCIsImZhbWlseU5hbWUiOiJET0UiLCJmaXJzdE5hbWUiOiJKYW5lIiwiZ2VuZGVyIjoiRkVNQUxFIiwibmFtZUFuZEZhbWlseU5hbWVBdEJpcnRoIjoiSmFuZSBET0UiLCJwZXJzb25hbElkZW50aWZpZXIiOiIwOTA0MDA4MDg0SCIsInBsYWNlT2ZCaXJ0aCI6IkxJTExFLCBGUkFOQ0UifSwiZXZpZGVuY2UiOlt7ImRvY3VtZW50UHJlc2VuY2UiOlsiUGh5c2ljYWwiXSwiZXZpZGVuY2VEb2N1bWVudCI6WyJQYXNzcG9ydCJdLCJzdWJqZWN0UHJlc2VuY2UiOiJQaHlzaWNhbCIsInR5cGUiOlsiRG9jdW1lbnRWZXJpZmljYXRpb24iXSwidmVyaWZpZXIiOiJkaWQ6ZWJzaToyQTlCWjlTVWU2QmF0YWNTcHZzMVY1Q2RqSHZMcFE3YkVzaTJKYjZMZEhLblF4YU4ifV19LCJqdGkiOiJ1cm46dXVpZDo1OWNlNGFjMi1lYzY1LTRiOGYtOGY5Ni1lYTc4NTEyZGY5ZDMifQ.6Wn8X2tEQJ9CmX3-meCxDuGmevRdtivnjVkGPXzfnJ-1M6AU4SFxxon0JmMjdmO_h4P9sCEe9RTtyTJou2yeCA', - format: 'jwt_vc', + 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6IiN6Nk1raDVITlBDQ0pXWm42V1JMalJQdHR5dllaQnNrWlVkU0pmVGlad2NVU2llcXgifQ.eyJ2Y3QiOiJBbmltb09wZW5JZDRWY1BsYXlncm91bmQiLCJwbGF5Z3JvdW5kIjp7ImZyYW1ld29yayI6IkFyaWVzIEZyYW1ld29yayBKYXZhU2NyaXB0IiwiY3JlYXRlZEJ5IjoiQW5pbW8gU29sdXRpb25zIiwiX3NkIjpbImZZM0ZqUHpZSEZOcHlZZnRnVl9kX25DMlRHSVh4UnZocE00VHdrMk1yMDQiLCJwTnNqdmZJeVBZOEQwTks1c1l0alR2Nkc2R0FNVDNLTjdaZDNVNDAwZ1pZIl19LCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoia2MydGxwaGNadzFBSUt5a3pNNnBjY2k2UXNLQW9jWXpGTC01RmUzNmg2RSJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1raDVITlBDQ0pXWm42V1JMalJQdHR5dllaQnNrWlVkU0pmVGlad2NVU2llcXgiLCJpYXQiOjE3MDU4NDM1NzQsIl9zZF9hbGciOiJzaGEtMjU2In0.2iAjaCFcuiHXTfQsrxXo6BghtwzqTrfDmhmarAAJAhY8r9yKXY3d10JY1dry2KnaEYWpq2R786thjdA5BXlPAQ~WyI5MzM3MTM0NzU4NDM3MjYyODY3NTE4NzkiLCJsYW5ndWFnZSIsIlR5cGVTY3JpcHQiXQ~WyIxMTQ3MDA5ODk2Nzc2MDYzOTc1MDUwOTMxIiwidmVyc2lvbiIsIjEuMCJd~', + format: 'vc+sd-jwt', + c_nonce: '98b487cb-f6e5-4f9b-b963-ad69b8fe5e29', + c_nonce_expires_in: 300000, }, } diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/openId4vc-holder-module.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/openId4vc-holder-module.test.ts index a160f1a7e3..4de88f6039 100644 --- a/packages/openid4vc/src/openid4vc-holder/__tests__/openId4vc-holder-module.test.ts +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/openId4vc-holder-module.test.ts @@ -1,9 +1,9 @@ -/* eslint-disable @typescript-eslint/unbound-method */ import type { DependencyManager } from '@aries-framework/core' -import { OpenId4VciHolderService, OpenId4VpHolderService } from '..' import { OpenId4VcHolderApi } from '../OpenId4VcHolderApi' import { OpenId4VcHolderModule } from '../OpenId4VcHolderModule' +import { OpenId4VciHolderService } from '../OpenId4VciHolderService' +import { OpenId4VpHolderService } from '../OpenId4VpHolderService' const dependencyManager = { registerInstance: jest.fn(), diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts index 0f31c93fdb..550c18040a 100644 --- a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts @@ -1,225 +1,89 @@ -import type { CredentialSupported, IssuerMetadata, PreAuthorizedCodeFlowConfig } from '../../openid4vc-issuer' -import type { KeyDidCreateOptions, VerificationMethod } from '@aries-framework/core' -import type { Server } from 'http' +import type { Key } from '@aries-framework/core' +import type { SdJwtVc } from '@aries-framework/sd-jwt-vc' import { AskarModule } from '@aries-framework/askar' import { + getJwkFromKey, Agent, - ClaimFormat, DidKey, JwaSignatureAlgorithm, KeyType, TypedArrayEncoder, - W3cCredential, - W3cCredentialRecord, - W3cCredentialSubject, - W3cIssuer, - w3cDate, + W3cJwtVerifiableCredential, } from '@aries-framework/core' import { agentDependencies } from '@aries-framework/node' -import { SdJwtCredential, SdJwtVcModule } from '@aries-framework/sd-jwt-vc' +import { SdJwtVcModule } from '@aries-framework/sd-jwt-vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' -import express, { Router, type Express } from 'express' import nock, { cleanAll, enableNetConnect } from 'nock' -import { OpenIdCredentialFormatProfile } from '..' -import { OpenId4VcIssuerModule } from '../../openid4vc-issuer' import { OpenId4VcHolderModule } from '../OpenId4VcHolderModule' -import { - mattrLaunchpadJsonLd_draft_08, - waltIdJffJwt_draft_08, - waltIdJffJwt_draft_11, - waltIssuerPortalV11, -} from './fixtures' - -const issuerPort = 1234 -const credentialIssuer = `http://localhost:${issuerPort}` - -const openBadgeCredential: CredentialSupported & { id: string } = { - id: `${credentialIssuer}/credentials/OpenBadgeCredential`, - format: OpenIdCredentialFormatProfile.JwtVcJson, - types: ['VerifiableCredential', 'OpenBadgeCredential'], -} - -const universityDegreeCredential: CredentialSupported & { id: string } = { - id: `${credentialIssuer}/credentials/UniversityDegreeCredential`, - format: OpenIdCredentialFormatProfile.JwtVcJson, - types: ['VerifiableCredential', 'UniversityDegreeCredential'], -} - -const universityDegreeCredentialLd: CredentialSupported & { id: string } = { - id: `${credentialIssuer}/credentials/UniversityDegreeCredentialLd`, - format: OpenIdCredentialFormatProfile.JwtVcJsonLd, - types: ['VerifiableCredential', 'UniversityDegreeCredential'], - '@context': ['context'], -} - -const universityDegreeCredentialSdJwt = { - id: 'https://openid4vc-issuer.com/credentials/UniversityDegreeCredentialSdJwt', - format: OpenIdCredentialFormatProfile.SdJwtVc, - credential_definition: { - vct: 'UniversityDegreeCredential', +import { animoOpenIdPlaygroundDraft11SdJwtVc, matrrLaunchpadDraft11JwtVcJson, waltIdDraft11JwtVcJson } from './fixtures' + +const holder = new Agent({ + config: { + label: 'OpenId4VcHolder Test28', + walletConfig: { id: 'openid4vc-holder-test27', key: 'openid4vc-holder-test27' }, }, -} satisfies CredentialSupported & { id: string } - -const baseCredentialRequestOptions = { - scheme: 'openid-credential-offer', - baseUri: credentialIssuer, -} - -const issuerMetadata: IssuerMetadata = { - issuerBaseUrl: credentialIssuer, - credentialEndpointPath: `/credentials`, - tokenEndpointPath: `/token`, - credentialsSupported: [openBadgeCredential, universityDegreeCredentialLd, universityDegreeCredentialSdJwt], -} - -const holderModules = { - openId4VcHolder: new OpenId4VcHolderModule(), - sdJwtVc: new SdJwtVcModule(), - askar: new AskarModule({ ariesAskar }), -} - -const issuerModules = { - openId4VcIssuer: new OpenId4VcIssuerModule({ issuerMetadata }), - sdJwtVc: new SdJwtVcModule(), - askar: new AskarModule({ ariesAskar }), -} + dependencies: agentDependencies, + modules: { + openId4VcHolder: new OpenId4VcHolderModule(), + sdJwtVc: new SdJwtVcModule(), + askar: new AskarModule({ ariesAskar }), + }, +}) describe('OpenId4VcHolder', () => { - let issuerApp: Express - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let issuerServer: Server - - let issuer: Agent - let issuerKid: string - let issuerDid: string - let issuerVerificationMethod: VerificationMethod - - let holder: Agent - let holderKid: string + let holderKey: Key let holderDid: string - let holderVerificationMethod: VerificationMethod - - let holderP256Kid: string - let holderP256Did: string - let holderP256VerificationMethod: VerificationMethod + let holderVerificationMethod: string beforeEach(async () => { - issuerApp = express() - - issuer = new Agent({ - config: { - label: 'OpenId4VcIssuer Test28', - walletConfig: { id: 'openid4vc-issuer-test27', key: 'openid4vc-issuer-test27' }, - }, - dependencies: agentDependencies, - modules: issuerModules, - }) - - holder = new Agent({ - config: { - label: 'OpenId4VcHolder Test28', - walletConfig: { id: 'openid4vc-holder-test27', key: 'openid4vc-holder-test27' }, - }, - dependencies: agentDependencies, - modules: holderModules, - }) - - await issuer.initialize() await holder.initialize() - const holderDidCreateResult = await holder.dids.create({ - method: 'key', - options: { keyType: KeyType.Ed25519 }, - secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, - }) - - holderDid = holderDidCreateResult.didState.did as string - const holderDidKey = DidKey.fromDid(holderDid) - holderKid = `${holderDid}#${holderDidKey.key.fingerprint}` - - const _holderVerificationMethod = holderDidCreateResult.didState.didDocument?.dereferenceKey(holderKid, [ - 'authentication', - ]) - if (!_holderVerificationMethod) throw new Error('No verification method found') - holderVerificationMethod = _holderVerificationMethod - - const holderP256DidCreateResult = await holder.dids.create({ - method: 'key', - options: { keyType: KeyType.P256 }, - secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e') }, + holderKey = await holder.wallet.createKey({ + keyType: KeyType.Ed25519, + privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598e'), }) - - holderP256Did = holderP256DidCreateResult.didState.did as string - const holderP256DidKey = DidKey.fromDid(holderP256Did) - holderP256Kid = `${holderP256Did}#${holderP256DidKey.key.fingerprint}` - - const _holderP256VerificationMethod = holderP256DidCreateResult.didState.didDocument?.dereferenceKey( - holderP256Kid, - ['authentication'] - ) - if (!_holderP256VerificationMethod) throw new Error('No verification method found') - holderP256VerificationMethod = _holderP256VerificationMethod - - const issuerDidCreateResult = await issuer.dids.create({ - method: 'key', - options: { keyType: KeyType.Ed25519 }, - secret: { privateKey: TypedArrayEncoder.fromString('96213c3d7fc8d4d6754c7a0fd969598f') }, - }) - - issuerDid = issuerDidCreateResult.didState.did as string - - const issuerDidKey = DidKey.fromDid(issuerDid) - issuerKid = `${issuerDid}#${issuerDidKey.key.fingerprint}` - const _issuerVerificationMethod = issuerDidCreateResult.didState.didDocument?.dereferenceKey(issuerKid, [ - 'authentication', - ]) - if (!_issuerVerificationMethod) throw new Error('No verification method found') - issuerVerificationMethod = _issuerVerificationMethod + const holderDidKey = new DidKey(holderKey) + holderDid = holderDidKey.did + holderVerificationMethod = `${holderDidKey.did}#${holderDidKey.key.fingerprint}` }) afterEach(async () => { - issuerServer?.close() - - await issuer.shutdown() - await issuer.wallet.delete() - await holder.shutdown() await holder.wallet.delete() }) - describe('[DRAFT 08]: Pre-authorized flow', () => { + describe('[DRAFT 11]: Pre-authorized flow', () => { afterEach(() => { cleanAll() enableNetConnect() }) - xit('[DRAFT 08]: Should successfully execute the pre-authorized flow using a did:key Ed25519 subject and JSON-LD credential', async () => { - const fixture = mattrLaunchpadJsonLd_draft_08 + it('Should successfully receive credential from MATTR launchpad using the pre-authorized flow using a did:key Ed25519 subject and jwt_vc_json credential', async () => { + const fixture = matrrLaunchpadDraft11JwtVcJson + /** * Below we're setting up some mock HTTP responses. * These responses are based on the openid-initiate-issuance URI above * */ // setup temporary redirect mock - nock('https://launchpad.mattrlabs.com') - .get('/.well-known/openid-credential-issuer') - .reply(307, undefined, { - Location: 'https://launchpad.vii.electron.mattrlabs.io/.well-known/openid-credential-issuer', - }) + nock('https://launchpad.mattrlabs.com').get('/.well-known/openid-credential-issuer').reply(307, undefined, { + Location: 'https://launchpad.vii.electron.mattrlabs.io/.well-known/openid-credential-issuer', + }) + + // setup server metadata response + nock('https://launchpad.vii.electron.mattrlabs.io') + .get('/.well-known/did.json') + .reply(200, fixture.wellKnownDid) + .get('/.well-known/openid-configuration') .reply(404) .get('/.well-known/oauth-authorization-server') .reply(404) - // setup server metadata response - nock('https://launchpad.vii.electron.mattrlabs.io') - .get('/.well-known/did.json') - .reply(200, fixture.wellKnownDid) - .get('/.well-known/did.json') - .reply(200, fixture.wellKnownDid) .get('/.well-known/openid-credential-issuer') .reply(200, fixture.getMetadataResponse) @@ -229,574 +93,203 @@ describe('OpenId4VcHolder', () => { // setup credential request response .post('/oidc/v1/auth/credential') - .reply(200, fixture.jsonLdCredentialResponse) - - const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer( - fixture.permanentResidentCardCredentialOffer - ) + .reply(200, fixture.credentialResponse) - const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( - resolved, - { - verifyCredentialStatus: false, - // We only allow EdDSa, as we've created a did with keyType ed25519. If we create - // or determine the did dynamically we could use any signature algorithm - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - credentialsToRequest: resolved.offeredCredentials.filter((c) => c.format === 'ldp_vc'), - proofOfPossessionVerificationMethodResolver: () => holderVerificationMethod, - } - ) + .get('/.well-known/did.json') + .reply(200, fixture.wellKnownDid) - expect(w3cCredentialRecords).toHaveLength(1) - const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord - expect(w3cCredentialRecord).toBeInstanceOf(W3cCredentialRecord) + const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer(fixture.credentialOffer) + const credentials = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode(resolved, { + verifyCredentialStatus: false, + // We only allow EdDSa, as we've created a did with keyType ed25519. If we create + // or determine the did dynamically we could use any signature algorithm + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + credentialsToRequest: resolved.offeredCredentials.filter((c) => c.format === 'jwt_vc_json'), + credentialBindingResolver: () => ({ method: 'did', didUrl: holderVerificationMethod }), + }) - expect(w3cCredentialRecord.credential.type).toEqual(['VerifiableCredential', 'PermanentResidentCard']) + expect(credentials).toHaveLength(1) + const w3cCredential = credentials[0] as W3cJwtVerifiableCredential + expect(w3cCredential).toBeInstanceOf(W3cJwtVerifiableCredential) - expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(holderDid) + expect(w3cCredential.credential.type).toEqual(['VerifiableCredential', 'OpenBadgeCredential']) + expect(w3cCredential.credential.credentialSubjectIds[0]).toEqual(holderDid) }) - it('[DRAFT 08]: Should successfully execute the pre-authorized flow using a did:key P256 subject and JWT credential', async () => { - const fixture = waltIdJffJwt_draft_08 + it('Should successfully receive credential from walt.id using the pre-authorized flow using a did:key Ed25519 subject and jwt_vc_json credential', async () => { + const fixture = waltIdDraft11JwtVcJson - nock('https://jff.walt.id/issuer-api/default/oidc') - // metadata - .get('/.well-known/openid-credential-issuer') - .reply(200, fixture.getMetadataResponse) + // setup server metadata response + nock('https://issuer.portal.walt.id') + // openid configuration is same as issuer metadata for walt.id .get('/.well-known/openid-configuration') - .reply(404) + .reply(200, fixture.getMetadataResponse) + .get('/.well-known/oauth-authorization-server') .reply(404) + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.getMetadataResponse) + // setup access token response .post('/token') - .reply(200, fixture.credentialResponse) + .reply(200, fixture.acquireAccessTokenResponse) // setup credential request response .post('/credential') .reply(200, fixture.credentialResponse) - const resolvedCredentialOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer( - fixture.credentialOffer - ) + const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer(fixture.credentialOffer) - const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( - resolvedCredentialOffer, - { - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], - proofOfPossessionVerificationMethodResolver: () => holderP256VerificationMethod, + await expect(() => + holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode(resolved, { verifyCredentialStatus: false, - credentialsToRequest: resolvedCredentialOffer.offeredCredentials.filter((credential) => { - return credential.format === 'jwt_vc_json' - }), - } + // We only allow EdDSa, as we've created a did with keyType ed25519. If we create + // or determine the did dynamically we could use any signature algorithm + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + credentialsToRequest: resolved.offeredCredentials.filter((c) => c.format === 'jwt_vc_json'), + credentialBindingResolver: () => ({ method: 'did', didUrl: holderVerificationMethod }), + }) ) - - expect(w3cCredentialRecords[0]).toBeInstanceOf(W3cCredentialRecord) - const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord - - expect(w3cCredentialRecord.credential.type).toEqual([ - 'VerifiableCredential', - 'VerifiableAttestation', - 'VerifiableId', - ]) - - expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(holderP256Did) + // FIXME: walt.id issues jwt where nbf and issuanceDate do not match + .rejects.toThrowError('JWT nbf and vc.issuanceDate do not match') }) - }) - describe('[DRAFT 08]: Authorization flow', () => { - afterAll(() => { - cleanAll() - enableNetConnect() - }) - - it('[DRAFT 08]: should throw if no scope and no authorization_details are provided', async () => { - const fixture = mattrLaunchpadJsonLd_draft_08 + it('Should successfully receive credential from animo openid4vc playground using the pre-authorized flow using a jwk EdDSA subject and vc+sd-jwt credential', async () => { + const fixture = animoOpenIdPlaygroundDraft11SdJwtVc - // setup temporary redirect mock - nock('https://launchpad.mattrlabs.com') - .get('/.well-known/openid-credential-issuer') - .reply(307, undefined, { - Location: 'https://launchpad.vii.electron.mattrlabs.io/.well-known/openid-credential-issuer', - }) - .get('/.well-known/openid-configuration') - .reply(404) + // setup server metadata response + nock('https://openid4vc.animo.id/oid4vci/0bbfb1c0-9f45-478c-a139-08f6ed610a37') .get('/.well-known/openid-configuration') .reply(404) - .get('/.well-known/openid-credential-issuer') - .reply(200, fixture.getMetadataResponse) - .get('/.well-known/oauth-authorization-server') - .reply(404) + .get('/.well-known/oauth-authorization-server') .reply(404) - // setup server metadata response - nock('https://launchpad.vii.electron.mattrlabs.io') - .get('/.well-known/did.json') - .reply(200, fixture.wellKnownDid) - .get('/.well-known/did.json') - .reply(200, fixture.wellKnownDid) .get('/.well-known/openid-credential-issuer') .reply(200, fixture.getMetadataResponse) - .get('/.well-known/openid-configuration') - .reply(404) - .get('/.well-known/oauth-authorization-server') - .reply(404) // setup access token response - .post('/oidc/v1/auth/token') + .post('/token') .reply(200, fixture.acquireAccessTokenResponse) // setup credential request response - .post('/oidc/v1/auth/credential') - .reply(200, fixture.jsonLdCredentialResponse) - - const clientId = 'test-client' - const redirectUri = 'https://example.com/cb' - - const resolvedOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer( - fixture.credentialOfferAuthorizationCodeFlow - ) - - const resolvedAuthRequest = await holder.modules.openId4VcHolder.resolveAuthorizationRequest(resolvedOffer, { - clientId, - redirectUri, - scope: ['openid'], - }) + .post('/credential') + .reply(200, fixture.credentialResponse) - await expect( - holder.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( - resolvedOffer, - resolvedAuthRequest, - 'code', - { - verifyCredentialStatus: false, - proofOfPossessionVerificationMethodResolver: () => holderVerificationMethod, - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - } - ) - ).rejects.toThrow() - }) + const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer(fixture.credentialOffer) - describe('[DRAFT 11]: Pre-authorized flow', () => { - afterEach(() => { - cleanAll() - enableNetConnect() + const credentials = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode(resolved, { + verifyCredentialStatus: false, + // We only allow EdDSa, as we've created a did with keyType ed25519. If we create + // or determine the did dynamically we could use any signature algorithm + allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], + credentialsToRequest: resolved.offeredCredentials.filter((c) => c.format === 'vc+sd-jwt'), + credentialBindingResolver: () => ({ method: 'jwk', jwk: getJwkFromKey(holderKey) }), }) - it('[DRAFT 11]: Should successfully execute the pre-authorized if no credential is requested', async () => { - const fixture = waltIdJffJwt_draft_11 - - /** - * Below we're setting up some mock HTTP responses. - * These responses are based on the openid-initiate-issuance URI above - */ - // setup server metadata response - nock('https://jff.walt.id/issuer-api/default/oidc') - .get('/.well-known/openid-credential-issuer') - .reply(200, fixture.getMetadataResponse) - .get('/.well-known/openid-configuration') - .reply(404) - .get('/.well-known/oauth-authorization-server') - .reply(404) - - const resolvedCredentialOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer( - fixture.credentialOffer - ) - - const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( - resolvedCredentialOffer, - { - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], - proofOfPossessionVerificationMethodResolver: () => holderVerificationMethod, - verifyCredentialStatus: false, - credentialsToRequest: [], - } - ) - - expect(w3cCredentialRecords).toHaveLength(0) - }) - - it('[DRAFT 11]: Should successfully execute the pre-authorized flow using a single offered credential a did:key ES256 subject and JwtVc format', async () => { - const fixture = waltIdJffJwt_draft_11 - const httpMock = nock('https://jff.walt.id/issuer-api/default/oidc') - .get('/.well-known/openid-credential-issuer') - .reply(200, fixture.getMetadataResponse) - .get('/.well-known/openid-configuration') - .reply(404) - .get('/.well-known/oauth-authorization-server') - .reply(404) - - // setup access token response - httpMock.post('/token').reply(200, fixture.acquireAccessTokenResponse) - // setup credential request response - httpMock.post('/credential').reply(200, fixture.credentialResponse) - - const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer(fixture.credentialOffer) - expect(resolved.offeredCredentials).toHaveLength(2) - - const selectedCredentialsForRequest = resolved.offeredCredentials.filter((credential) => { - return credential.format === 'jwt_vc_json' && credential.types.includes('VerifiableId') - }) - - expect(selectedCredentialsForRequest).toHaveLength(1) - - const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( - resolved, - { - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.ES256], - proofOfPossessionVerificationMethodResolver: () => holderP256VerificationMethod, - verifyCredentialStatus: false, - credentialsToRequest: selectedCredentialsForRequest, - } - ) - - expect(w3cCredentialRecords).toHaveLength(1) - expect(w3cCredentialRecords[0]).toBeInstanceOf(W3cCredentialRecord) - const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord - - expect(w3cCredentialRecord.credential.type).toEqual([ - 'VerifiableCredential', - 'VerifiableAttestation', - 'VerifiableId', - ]) - - expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(holderP256Did) + expect(credentials).toHaveLength(1) + const credential = credentials[0] as SdJwtVc + expect(credential).toEqual({ + compact: + 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6IiN6Nk1raDVITlBDQ0pXWm42V1JMalJQdHR5dllaQnNrWlVkU0pmVGlad2NVU2llcXgifQ.eyJ2Y3QiOiJBbmltb09wZW5JZDRWY1BsYXlncm91bmQiLCJwbGF5Z3JvdW5kIjp7ImZyYW1ld29yayI6IkFyaWVzIEZyYW1ld29yayBKYXZhU2NyaXB0IiwiY3JlYXRlZEJ5IjoiQW5pbW8gU29sdXRpb25zIiwiX3NkIjpbImZZM0ZqUHpZSEZOcHlZZnRnVl9kX25DMlRHSVh4UnZocE00VHdrMk1yMDQiLCJwTnNqdmZJeVBZOEQwTks1c1l0alR2Nkc2R0FNVDNLTjdaZDNVNDAwZ1pZIl19LCJjbmYiOnsiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4Ijoia2MydGxwaGNadzFBSUt5a3pNNnBjY2k2UXNLQW9jWXpGTC01RmUzNmg2RSJ9fSwiaXNzIjoiZGlkOmtleTp6Nk1raDVITlBDQ0pXWm42V1JMalJQdHR5dllaQnNrWlVkU0pmVGlad2NVU2llcXgiLCJpYXQiOjE3MDU4NDM1NzQsIl9zZF9hbGciOiJzaGEtMjU2In0.2iAjaCFcuiHXTfQsrxXo6BghtwzqTrfDmhmarAAJAhY8r9yKXY3d10JY1dry2KnaEYWpq2R786thjdA5BXlPAQ~WyI5MzM3MTM0NzU4NDM3MjYyODY3NTE4NzkiLCJsYW5ndWFnZSIsIlR5cGVTY3JpcHQiXQ~WyIxMTQ3MDA5ODk2Nzc2MDYzOTc1MDUwOTMxIiwidmVyc2lvbiIsIjEuMCJd~', + header: { + alg: 'EdDSA', + kid: '#z6Mkh5HNPCCJWZn6WRLjRPttyvYZBskZUdSJfTiZwcUSieqx', + typ: 'vc+sd-jwt', + }, + payload: { + _sd_alg: 'sha-256', + cnf: { + jwk: { + crv: 'Ed25519', + kty: 'OKP', + x: 'kc2tlphcZw1AIKykzM6pcci6QsKAocYzFL-5Fe36h6E', + }, + }, + iat: 1705843574, + iss: 'did:key:z6Mkh5HNPCCJWZn6WRLjRPttyvYZBskZUdSJfTiZwcUSieqx', + playground: { + _sd: ['fY3FjPzYHFNpyYftgV_d_nC2TGIXxRvhpM4Twk2Mr04', 'pNsjvfIyPY8D0NK5sYtjTv6G6GAMT3KN7Zd3U400gZY'], + createdBy: 'Animo Solutions', + framework: 'Aries Framework JavaScript', + }, + vct: 'AnimoOpenId4VcPlayground', + }, + prettyClaims: { + cnf: { + jwk: { + crv: 'Ed25519', + kty: 'OKP', + x: 'kc2tlphcZw1AIKykzM6pcci6QsKAocYzFL-5Fe36h6E', + }, + }, + iat: 1705843574, + iss: 'did:key:z6Mkh5HNPCCJWZn6WRLjRPttyvYZBskZUdSJfTiZwcUSieqx', + playground: { + createdBy: 'Animo Solutions', + framework: 'Aries Framework JavaScript', + language: 'TypeScript', + version: '1.0', + }, + vct: 'AnimoOpenId4VcPlayground', + }, }) + }) + }) - xit('[DRAFT 11]: Should successfully execute the pre-authorized flow using a single offered credential a did:key EdDSA subject and JsonLd format', async () => { - const fixture = waltIdJffJwt_draft_11 - const httpMock = nock('https://jff.walt.id/issuer-api/default/oidc') - .get('/.well-known/openid-credential-issuer') - .reply(200, fixture.getMetadataResponse) - .get('/.well-known/openid-configuration') - .reply(404) - .get('/.well-known/oauth-authorization-server') - .reply(404) - - // setup access token response - httpMock.post('/token').reply(200, fixture.acquireAccessTokenResponse) - // setup credential request response - httpMock.post('/credential').reply(200, fixture.jsonLdCredentialResponse) - - const resolvedCredentialOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer( - fixture.credentialOffer - ) - - expect(resolvedCredentialOffer.offeredCredentials).toHaveLength(2) - const selectedCredentialsForRequest = resolvedCredentialOffer.offeredCredentials.filter((credential) => { - return ( - credential.format === OpenIdCredentialFormatProfile.LdpVc && credential.types.includes('VerifiableDiploma') - ) - }) - - expect(selectedCredentialsForRequest).toHaveLength(1) - - const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( - resolvedCredentialOffer, - { - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - proofOfPossessionVerificationMethodResolver: () => holderVerificationMethod, - verifyCredentialStatus: false, - credentialsToRequest: selectedCredentialsForRequest, - } - ) - - expect(w3cCredentialRecords).toHaveLength(1) - expect(w3cCredentialRecords[0]).toBeInstanceOf(W3cCredentialRecord) - const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord - - expect(w3cCredentialRecord.credential.type).toEqual(['VerifiableCredential', 'PermanentResidentCard']) - - expect(w3cCredentialRecord.credential.credentialSubjectIds[0]).toEqual(holderDid) - }) + describe('[DRAFT 11]: Authorization flow', () => { + afterAll(() => { + cleanAll() + enableNetConnect() + }) - xit('[DRAFT 11]: Should successfully execute the pre-authorized for multiple credentials of different formats using a did:key EdDsa subject', async () => { - const fixture = waltIdJffJwt_draft_11 - const httpMock = nock('https://jff.walt.id/issuer-api/default/oidc') - .get('/.well-known/openid-credential-issuer') - .reply(200, fixture.getMetadataResponse) - .get('/.well-known/openid-configuration') - .reply(404) - .get('/.well-known/oauth-authorization-server') - .reply(404) + it('Should successfully receive credential from walt.id using the authorized flow using a did:key Ed25519 subject and jwt_vc_json credential', async () => { + const fixture = waltIdDraft11JwtVcJson + // setup temporary redirect mock + nock('https://issuer.portal.walt.id') + .get('/.well-known/openid-credential-issuer') + .reply(200, fixture.getMetadataResponse) + .get('/.well-known/openid-configuration') + .reply(200, fixture.getMetadataResponse) + .get('/.well-known/oauth-authorization-server') + .reply(404) + .post('/par') + .reply(200, fixture.par) // setup access token response - httpMock.post('/token').reply(200, fixture.credentialResponse) + .post('/token') + .reply(200, fixture.acquireAccessTokenResponse) // setup credential request response - httpMock.post('/credential').reply(200, fixture.credentialResponse) - httpMock.post('/credential').reply(200, fixture.jsonLdCredentialResponse) + .post('/credential') + .reply(200, fixture.credentialResponse) - const resolvedCredentialOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer( - fixture.credentialOffer - ) + .get('/.well-known/oauth-authorization-server') + .reply(404) - expect(resolvedCredentialOffer.offeredCredentials).toHaveLength(2) + const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer(fixture.credentialOffer) - const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( - resolvedCredentialOffer, - { - allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - proofOfPossessionVerificationMethodResolver: () => holderVerificationMethod, - verifyCredentialStatus: false, - } - ) - - expect(w3cCredentialRecords.length).toEqual(2) - expect(w3cCredentialRecords[0]).toBeInstanceOf(W3cCredentialRecord) - const w3cCredentialRecord = w3cCredentialRecords[0] as W3cCredentialRecord - expect(w3cCredentialRecord.credential.claimFormat).toEqual(ClaimFormat.JwtVc) - expect(w3cCredentialRecord.credential.type).toEqual([ - 'VerifiableCredential', - 'VerifiableAttestation', - 'VerifiableId', - ]) - - expect(w3cCredentialRecords[1]).toBeInstanceOf(W3cCredentialRecord) - const w3cCredentialRecord1 = w3cCredentialRecords[1] as W3cCredentialRecord - expect(w3cCredentialRecord1.credential.claimFormat).toEqual(ClaimFormat.LdpVc) - expect(w3cCredentialRecord1.credential.type).toEqual(['VerifiableCredential', 'PermanentResidentCard']) - expect(w3cCredentialRecord1.credential.credentialSubjectIds[0]).toEqual(holderDid) + const resolvedAuthorizationRequest = await holder.modules.openId4VcHolder.resolveAuthorizationRequest(resolved, { + clientId: 'test-client', + redirectUri: 'http://example.com', + scope: ['openid', 'UniversityDegree'], }) - it('authorization code flow https://portal.walt.id/', async () => { - const fixture = waltIssuerPortalV11 - // setup temporary redirect mock - nock('https://issuer.portal.walt.id') - .get('/.well-known/openid-credential-issuer') - .reply(200, fixture.issuerMetadata) - .get('/.well-known/openid-configuration') - .reply(404) - .get('/.well-known/oauth-authorization-server') - .reply(404) - .post('/par') - .reply(200, fixture.par) - // setup access token response - .post('/token') - .reply(200, fixture.acquireAccessTokenResponse) - // setup credential request response - .post('/credential') - .reply(200, fixture.credentialResponse) - - .get('/.well-known/oauth-authorization-server') - .reply(404) - - const credentialOffer = `openid-credential-offer://?credential_offer=%7B%22credential_issuer%22%3A%22https%3A%2F%2Fissuer.portal.walt.id%22%2C%22credentials%22%3A%5B%7B%22format%22%3A%22jwt_vc_json%22%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22OpenBadgeCredential%22%5D%2C%22credential_definition%22%3A%7B%22%40context%22%3A%5B%22https%3A%2F%2Fwww.w3.org%2F2018%2Fcredentials%2Fv1%22%2C%22https%3A%2F%2Fpurl.imsglobal.org%2Fspec%2Fob%2Fv3p0%2Fcontext.json%22%5D%2C%22types%22%3A%5B%22VerifiableCredential%22%2C%22OpenBadgeCredential%22%5D%7D%7D%5D%2C%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%22b0e16785-d722-42a5-a04f-4beab28e03ea%22%7D%2C%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJiMGUxNjc4NS1kNzIyLTQyYTUtYTA0Zi00YmVhYjI4ZTAzZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.ibEpHFaHFBLWyhEf4SotDQZBeh_FMrfncWapNox1Iv1kdQWQ2cLQeS1VrCyVmPsbx0tN2MAyDFG7DnAaq8MiAA%22%2C%22user_pin_required%22%3Afalse%7D%7D%7D` - const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) - - const resolvedAuthorizationRequest = await holder.modules.openId4VcHolder.resolveAuthorizationRequest( - resolved, - { - clientId: 'test-client', - redirectUri: 'http://blank', - scope: ['openid', 'OpenBadgeCredential'], - } - ) - - const code = - 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJiMGUxNjc4NS1kNzIyLTQyYTUtYTA0Zi00YmVhYjI4ZTAzZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.ibEpHFaHFBLWyhEf4SotDQZBeh_FMrfncWapNox1Iv1kdQWQ2cLQeS1VrCyVmPsbx0tN2MAyDFG7DnAaq8MiAA' - - const w3cCredentialRecords = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( + await expect( + holder.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( resolved, resolvedAuthorizationRequest, - code, + fixture.authorizationCode, { allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - proofOfPossessionVerificationMethodResolver: () => holderVerificationMethod, + credentialBindingResolver: () => ({ method: 'did', didUrl: holderVerificationMethod }), verifyCredentialStatus: false, } ) - - expect(w3cCredentialRecords).toHaveLength(1) - }) - - it('e2e flow with issuer endpoints requesting multiple credentials', async () => { - const router = Router() - await issuer.modules.openId4VcIssuer.configureRouter(router, { - basePath: '/', - metadataEndpointConfig: { enabled: true }, - accessTokenEndpointConfig: { - enabled: true, - preAuthorizedCodeExpirationDuration: 50, - verificationMethod: issuerVerificationMethod, - }, - credentialEndpointConfig: { - enabled: true, - verificationMethod: issuerVerificationMethod, - credentialRequestToCredentialMapper: async ({ credentialRequest, holderDid }) => { - if ( - credentialRequest.format === 'jwt_vc_json' && - credentialRequest.types.includes('OpenBadgeCredential') - ) { - if (holderDid !== holderDid) throw new Error('Invalid holder did') - - return new W3cCredential({ - type: openBadgeCredential.types, - issuer: new W3cIssuer({ id: issuerDid }), - credentialSubject: new W3cCredentialSubject({ id: holderDid }), - issuanceDate: w3cDate(Date.now()), - }) - } - - if ( - credentialRequest.format === 'jwt_vc_json' && - credentialRequest.types.includes('UniversityDegreeCredential') - ) { - return new W3cCredential({ - type: universityDegreeCredential.types, - issuer: new W3cIssuer({ id: issuerDid }), - credentialSubject: new W3cCredentialSubject({ id: holderDid }), - issuanceDate: w3cDate(Date.now()), - }) - } - throw new Error('Invalid request') - }, - }, - }) - - issuerApp.use('/', router) - issuerServer = issuerApp.listen(issuerPort) - - const preAuthorizedCodeFlowConfig: PreAuthorizedCodeFlowConfig = { - preAuthorizedCode: '123456789', - userPinRequired: false, - } - - const { credentialOfferRequest } = await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest( - [ - openBadgeCredential.id, - { - format: universityDegreeCredential.format, - types: universityDegreeCredential.types, - }, - ], - { - preAuthorizedCodeFlowConfig, - ...baseCredentialRequestOptions, - baseUri: '', - } - ) - - const resolvedCredentialOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer( - credentialOfferRequest - ) - - const credentials = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( - resolvedCredentialOffer, - { - proofOfPossessionVerificationMethodResolver: async () => { - return holderVerificationMethod - }, - } - ) - - expect(credentials).toHaveLength(2) - expect(credentials[0]).toBeInstanceOf(W3cCredentialRecord) - if (credentials[0].type === 'SdJwtVcRecord') throw new Error('Invalid credential type') - if (credentials[1].type === 'SdJwtVcRecord') throw new Error('Invalid credential type') - - expect(credentials[0].credential.type).toHaveLength(2) - expect(credentials[1].credential.type).toHaveLength(2) - - if (credentials[0].credential.type.includes('OpenBadgeCredential')) { - expect(credentials[0].credential.type).toEqual(['VerifiableCredential', 'OpenBadgeCredential']) - expect(credentials[1].credential.type).toEqual(['VerifiableCredential', 'UniversityDegreeCredential']) - } else { - expect(credentials[1].credential.type).toEqual(['VerifiableCredential', 'OpenBadgeCredential']) - expect(credentials[0].credential.type).toEqual(['VerifiableCredential', 'UniversityDegreeCredential']) - } - }) - }) - - it('e2e flow with issuer endpoints requesting sdjwtvc', async () => { - const router = Router() - await issuer.modules.openId4VcIssuer.configureRouter(router, { - basePath: '/', - metadataEndpointConfig: { enabled: true }, - accessTokenEndpointConfig: { - enabled: true, - preAuthorizedCodeExpirationDuration: 50, - verificationMethod: issuerVerificationMethod, - }, - credentialEndpointConfig: { - enabled: true, - verificationMethod: issuerVerificationMethod, - credentialRequestToCredentialMapper: async ({ credentialRequest, holderDid, holderDidUrl }) => { - if ( - credentialRequest.format === 'vc+sd-jwt' && - credentialRequest.credential_definition.vct === 'UniversityDegreeCredential' - ) { - if (holderDid !== holderDid) throw new Error('Invalid holder did') - - return new SdJwtCredential({ - payload: { type: 'UniversityDegreeCredential', university: 'innsbruck', degree: 'bachelor' }, - holderDidUrl: holderDidUrl, - issuerDidUrl: issuerKid, - disclosureFrame: { university: true, degree: true }, - }) - } - throw new Error('Invalid request') - }, - }, - }) - - issuerApp.use('/', router) - issuerServer = issuerApp.listen(issuerPort) - - const { credentialOfferRequest } = await issuer.modules.openId4VcIssuer.createCredentialOfferAndRequest( - [universityDegreeCredentialSdJwt.id], - { preAuthorizedCodeFlowConfig: { userPinRequired: false }, ...baseCredentialRequestOptions } - ) - - const resolvedCredentialOffer = await holder.modules.openId4VcHolder.resolveCredentialOffer( - credentialOfferRequest - ) - - const credentials = await holder.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( - resolvedCredentialOffer, - { - proofOfPossessionVerificationMethodResolver: async () => { - return holderVerificationMethod - }, - } ) - - expect(credentials).toHaveLength(1) - if (credentials[0].type === 'W3cCredentialRecord') throw new Error('Invalid credential type') - expect(credentials[0].sdJwtVc.payload['type']).toEqual('UniversityDegreeCredential') + // FIXME: credential returned by walt.id has nbf and issuanceDate that do not match + // but we know that we at least received the credential if we got to this error + .rejects.toThrowError('JWT nbf and vc.issuanceDate do not match') }) - - //it('authorization code flow https://portal.walt.id/', async () => { - // const credentialOffer = `` - - // const didKey = DidKey.fromDid(did.didState.did as string) - // const kid = `${didKey.did}#${didKey.key.fingerprint}` - // const verificationMethod = did.didState.didDocument?.dereferenceKey(kid, ['authentication']) - // if (!verificationMethod) throw new Error('No verification method found') - - // const resolved = await agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer) - - // const resolvedAuthorizationRequest = await agent.modules.openId4VcHolder.resolveAuthorizationRequest(resolved, { - // clientId: 'test-client', - // redirectUri: 'http://blank', - // }) - - // const code = - // 'eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJiMGUxNjc4NS1kNzIyLTQyYTUtYTA0Zi00YmVhYjI4ZTAzZWEiLCJpc3MiOiJodHRwczovL2lzc3Vlci5wb3J0YWwud2FsdC5pZCIsImF1ZCI6IlRPS0VOIn0.ibEpHFaHFBLWyhEf4SotDQZBeh_FMrfncWapNox1Iv1kdQWQ2cLQeS1VrCyVmPsbx0tN2MAyDFG7DnAaq8MiAA' - - // const w3cCredentialRecords = await agent.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( - // resolved, - // resolvedAuthorizationRequest, - // code, - // { - // allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - // proofOfPossessionVerificationMethodResolver: () => verificationMethod, - // verifyCredentialStatus: false, - // } - // ) - - // expect(w3cCredentialRecords).toHaveLength(1) - //}) }) }) diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierApi.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierApi.ts index 612a006383..b2dc64facb 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierApi.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierApi.ts @@ -79,7 +79,7 @@ export class OpenId4VcVerifierApi { verifierId: string }): Promise { const verifier = await this.getByVerifierId(verifierId) - return await this.openId4VcVerifierService.verifyProofResponse(this.agentContext, { + return await this.openId4VcVerifierService.verifyAuthorizationResponse(this.agentContext, { ...otherOptions, verifier, }) From 590895c48efcde6ce61a566c444e504bf45756ee Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 22 Jan 2024 16:37:30 +0700 Subject: [PATCH 104/115] expose signing methods on w3c service Signed-off-by: Timo Glastra --- .../src/modules/vc/W3cCredentialService.ts | 24 +++++++------ .../modules/vc/W3cCredentialServiceOptions.ts | 21 +++++++++-- .../core/src/modules/vc/W3cCredentialsApi.ts | 35 +++++++++++++++++-- 3 files changed, 65 insertions(+), 15 deletions(-) diff --git a/packages/core/src/modules/vc/W3cCredentialService.ts b/packages/core/src/modules/vc/W3cCredentialService.ts index 1cca4272de..035a997f50 100644 --- a/packages/core/src/modules/vc/W3cCredentialService.ts +++ b/packages/core/src/modules/vc/W3cCredentialService.ts @@ -54,14 +54,16 @@ export class W3cCredentialService { * @param credential the credential to be signed * @returns the signed credential */ - public async signCredential( + public async signCredential( agentContext: AgentContext, - options: W3cSignCredentialOptions - ): Promise> { + options: W3cSignCredentialOptions + ): Promise> { if (options.format === ClaimFormat.JwtVc) { - return this.w3cJwtCredentialService.signCredential(agentContext, options) + const signed = await this.w3cJwtCredentialService.signCredential(agentContext, options) + return signed as W3cVerifiableCredential } else if (options.format === ClaimFormat.LdpVc) { - return this.w3cJsonLdCredentialService.signCredential(agentContext, options) + const signed = await this.w3cJsonLdCredentialService.signCredential(agentContext, options) + return signed as W3cVerifiableCredential } else { throw new AriesFrameworkError(`Unsupported format in options. Format must be either 'jwt_vc' or 'ldp_vc'`) } @@ -110,14 +112,16 @@ export class W3cCredentialService { * @param presentation the presentation to be signed * @returns the signed presentation */ - public async signPresentation( + public async signPresentation( agentContext: AgentContext, - options: W3cSignPresentationOptions - ): Promise> { + options: W3cSignPresentationOptions + ): Promise> { if (options.format === ClaimFormat.JwtVp) { - return this.w3cJwtCredentialService.signPresentation(agentContext, options) + const signed = await this.w3cJwtCredentialService.signPresentation(agentContext, options) + return signed as W3cVerifiablePresentation } else if (options.format === ClaimFormat.LdpVp) { - return this.w3cJsonLdCredentialService.signPresentation(agentContext, options) + const signed = await this.w3cJsonLdCredentialService.signPresentation(agentContext, options) + return signed as W3cVerifiablePresentation } else { throw new AriesFrameworkError(`Unsupported format in options. Format must be either 'jwt_vp' or 'ldp_vp'`) } diff --git a/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts b/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts index 6f15025e1c..3a9b892e89 100644 --- a/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts +++ b/packages/core/src/modules/vc/W3cCredentialServiceOptions.ts @@ -8,9 +8,24 @@ import type { W3cPresentation } from './models/presentation/W3cPresentation' import type { JwaSignatureAlgorithm } from '../../crypto/jose/jwa' import type { SingleOrArray } from '../../utils/type' -export type W3cSignCredentialOptions = W3cJwtSignCredentialOptions | W3cJsonLdSignCredentialOptions -export type W3cVerifyCredentialOptions = W3cJwtVerifyCredentialOptions | W3cJsonLdVerifyCredentialOptions -export type W3cSignPresentationOptions = W3cJwtSignPresentationOptions | W3cJsonLdSignPresentationOptions +export type W3cSignCredentialOptions = + Format extends ClaimFormat.JwtVc + ? W3cJwtSignCredentialOptions + : Format extends ClaimFormat.LdpVc + ? W3cJsonLdSignCredentialOptions + : W3cJwtSignCredentialOptions | W3cJsonLdSignCredentialOptions +export type W3cVerifyCredentialOptions = + Format extends ClaimFormat.JwtVc + ? W3cJwtVerifyCredentialOptions + : Format extends ClaimFormat.LdpVc + ? W3cJsonLdVerifyCredentialOptions + : W3cJwtVerifyCredentialOptions | W3cJsonLdVerifyCredentialOptions +export type W3cSignPresentationOptions = + Format extends ClaimFormat.JwtVp + ? W3cJwtSignPresentationOptions + : Format extends ClaimFormat.LdpVp + ? W3cJsonLdSignPresentationOptions + : W3cJwtSignPresentationOptions | W3cJsonLdSignPresentationOptions export type W3cVerifyPresentationOptions = W3cJwtVerifyPresentationOptions | W3cJsonLdVerifyPresentationOptions interface W3cSignCredentialOptionsBase { diff --git a/packages/core/src/modules/vc/W3cCredentialsApi.ts b/packages/core/src/modules/vc/W3cCredentialsApi.ts index bb86cbb8f5..a378974992 100644 --- a/packages/core/src/modules/vc/W3cCredentialsApi.ts +++ b/packages/core/src/modules/vc/W3cCredentialsApi.ts @@ -1,5 +1,12 @@ -import type { StoreCredentialOptions } from './W3cCredentialServiceOptions' -import type { W3cVerifiableCredential } from './models' +import type { + StoreCredentialOptions, + W3cCreatePresentationOptions, + W3cSignCredentialOptions, + W3cSignPresentationOptions, + W3cVerifyCredentialOptions, + W3cVerifyPresentationOptions, +} from './W3cCredentialServiceOptions' +import type { W3cVerifiableCredential, ClaimFormat } from './models' import type { W3cCredentialRecord } from './repository' import type { Query } from '../../storage/StorageService' @@ -40,4 +47,28 @@ export class W3cCredentialsApi { public async findCredentialRecordsByQuery(query: Query): Promise { return this.w3cCredentialService.findCredentialsByQuery(this.agentContext, query) } + + public async signCredential( + options: W3cSignCredentialOptions + ) { + return this.w3cCredentialService.signCredential(this.agentContext, options) + } + + public async verifyCredential(options: W3cVerifyCredentialOptions) { + return this.w3cCredentialService.verifyCredential(this.agentContext, options) + } + + public async createPresentation(options: W3cCreatePresentationOptions) { + return this.w3cCredentialService.createPresentation(options) + } + + public async signPresentation( + options: W3cSignPresentationOptions + ) { + return this.w3cCredentialService.signPresentation(this.agentContext, options) + } + + public async verifyPresentation(options: W3cVerifyPresentationOptions) { + return this.w3cCredentialService.verifyPresentation(this.agentContext, options) + } } From 3c5f5206e9b79eed2094d3ff9f128cecfe9a6581 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Mon, 22 Jan 2024 16:43:42 +0700 Subject: [PATCH 105/115] fix a bunch of tests Signed-off-by: Timo Glastra --- packages/openid4vc/package.json | 6 +- .../OpenId4VciHolderService.ts | 5 +- .../OpenId4VpHolderService.ts | 2 +- .../openid4vc/src/openid4vc-holder/index.ts | 6 +- .../openid4vc-issuer/OpenId4VcIssuerModule.ts | 8 + .../OpenId4VcVerifierApi.ts | 6 +- .../OpenId4VcVerifierModule.ts | 10 + .../OpenId4VcVerifierModuleConfig.ts | 4 +- .../OpenId4VcVerifierServiceOptions.ts | 16 +- .../router/authorizationEndpoint.ts | 9 +- .../openid4vc/tests/openid4vc.e2e.test.ts | 474 +++++++++--------- packages/openid4vc/tests/utilsVp.ts | 18 +- 12 files changed, 307 insertions(+), 257 deletions(-) diff --git a/packages/openid4vc/package.json b/packages/openid4vc/package.json index e3d4d8a6ad..931e7fce18 100644 --- a/packages/openid4vc/package.json +++ b/packages/openid4vc/package.json @@ -24,8 +24,8 @@ "test": "jest --forceExit --detectOpenHandles" }, "dependencies": { - "@aries-framework/askar": "^0.4.2", "@aries-framework/core": "0.4.2", + "@aries-framework/openid4vc": "0.4.2", "@sphereon/ssi-types": "^0.18.1", "@sphereon/oid4vci-client": "0.8.2-next.34", "@sphereon/oid4vci-common": "0.8.2-next.34", @@ -33,10 +33,6 @@ "@sphereon/did-auth-siop": "0.6.0-unstable.0" }, "devDependencies": { - "@aries-framework/node": "^0.4.2", - "@aries-framework/sd-jwt-vc": "^0.4.2", - "@aries-framework/tenants": "^0.4.2", - "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1", "@types/express": "^4.17.21", "express": "^4.18.2", "nock": "^13.3.0", diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts index e1c767d814..273e8b66a3 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts @@ -589,7 +589,10 @@ export class OpenId4VciHolderService { ) const sdJwtVcApi = getApiForModuleByName(agentContext, 'SdJwtVcModule') - if (!sdJwtVcApi) throw new AriesFrameworkError(`Could not find the SdJwtVcApi`) + if (!sdJwtVcApi) + throw new AriesFrameworkError( + `Could not find the SdJwtVcApi. Make sure the @aries-framework/sd-jwt-vc module is registered.` + ) const { verification, sdJwtVc } = await sdJwtVcApi.verify({ compactSdJwtVc: credentialResponse.successBody.credential, }) diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderService.ts index 6a08a400d2..8f852069bb 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderService.ts @@ -182,7 +182,7 @@ export class OpenId4VpHolderService { const verificationMethod = await this.getVerificationMethodFromVerifiablePresentation( agentContext, - verifiablePresentations[0] as W3cVerifiablePresentation + verifiablePresentations[0] ) const openidProvider = await this.getOpenIdProvider(agentContext, { verificationMethod }) diff --git a/packages/openid4vc/src/openid4vc-holder/index.ts b/packages/openid4vc/src/openid4vc-holder/index.ts index fd3fb09683..ab5529210a 100644 --- a/packages/openid4vc/src/openid4vc-holder/index.ts +++ b/packages/openid4vc/src/openid4vc-holder/index.ts @@ -1,4 +1,6 @@ export * from './OpenId4VcHolderApi' export * from './OpenId4VcHolderModule' -export * from './presentation' -export * from './reception' +export * from './OpenId4VciHolderService' +export * from './OpenId4VciHolderServiceOptions' +export * from './OpenId4VpHolderService' +export * from './OpenId4VpHolderServiceOptions' diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts index b02363dc8a..3ff05b75a4 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts @@ -57,6 +57,13 @@ export class OpenId4VcIssuerModule implements Module { private configureRouter(rootAgentContext: AgentContext) { const { Router, json, urlencoded } = importExpress() + // FIXME: it is currently not possible to initialize an agent + // shut it down, and then start it again, as the + // express router is configured with a specific `AgentContext` instance + // and dependency manager. One option is to always create a new router + // but then users cannot pass their own router implementation. + // We need to find a proper way to fix this. + // We use separate context router and endpoint router. Context router handles the linking of the request // to a specific agent context. Endpoint router only knows about a single context const endpointRouter = Router() @@ -69,6 +76,7 @@ export class OpenId4VcIssuerModule implements Module { contextRouter.param('issuerId', async (req: OpenId4VcIssuanceRequest, _res, next, issuerId: string) => { if (!issuerId) { + rootAgentContext.config.logger.debug('No issuerId provided for incoming oid4vci request, returning 404') _res.status(404).send('Not found') } diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierApi.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierApi.ts index b2dc64facb..46efcc3d2c 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierApi.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierApi.ts @@ -54,12 +54,12 @@ export class OpenId4VcVerifierApi { * See {@link OpenId4VcCreateAuthorizationRequestOptions} for detailed documentation on the options. */ public async createAuthorizationRequest({ - verifiedId, + verifierId, ...otherOptions }: OpenId4VcCreateAuthorizationRequestOptions & { - verifiedId: string + verifierId: string }): Promise { - const verifier = await this.getByVerifierId(verifiedId) + const verifier = await this.getByVerifierId(verifierId) return await this.openId4VcVerifierService.createAuthorizationRequest(this.agentContext, { ...otherOptions, verifier, diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts index e6ee9733df..495141d6c7 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts @@ -56,6 +56,13 @@ export class OpenId4VcVerifierModule implements Module { private configureRouter(rootAgentContext: AgentContext) { const { Router, json, urlencoded } = importExpress() + // FIXME: it is currently not possible to initialize an agent + // shut it down, and then start it again, as the + // express router is configured with a specific `AgentContext` instance + // and dependency manager. One option is to always create a new router + // but then users cannot pass their own router implementation. + // We need to find a proper way to fix this. + // We use separate context router and endpoint router. Context router handles the linking of the request // to a specific agent context. Endpoint router only knows about a single context const endpointRouter = Router() @@ -68,6 +75,9 @@ export class OpenId4VcVerifierModule implements Module { contextRouter.param('verifierId', async (req: OpenId4VcVerificationRequest, _res, next, verifierId: string) => { if (!verifierId) { + rootAgentContext.config.logger.debug( + 'No verifierId provided for incoming authorization response, returning 404' + ) _res.status(404).send('Not found') } diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts index bc29abc1c5..05a49a32d1 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts @@ -25,7 +25,7 @@ export interface OpenId4VcVerifierModuleConfigOptions { */ router?: Router - endpoints: { + endpoints?: { // FIXME: interface name with openid4vc prefix authorization?: Optional } @@ -55,7 +55,7 @@ export class OpenId4VcVerifierModuleConfig { public get authorizationEndpoint(): AuthorizationEndpointConfig { // Use user supplied options, or return defaults. - const userOptions = this.options.endpoints.authorization + const userOptions = this.options.endpoints?.authorization return { ...userOptions, diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierServiceOptions.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierServiceOptions.ts index c3f8a88e5b..7dd4609386 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierServiceOptions.ts @@ -1,4 +1,10 @@ -import type { VerificationMethod } from '@aries-framework/core' +import type { + DifPresentationExchangeDefinition, + DifPresentationExchangeSubmission, + VerificationMethod, + W3cVerifiablePresentation, +} from '@aries-framework/core' +import type { SdJwtVc } from '@aries-framework/sd-jwt-vc' import type { IDTokenPayload, VerifiedOpenID4VPSubmission, @@ -73,7 +79,13 @@ export interface VerifyProofResponseOptions { export interface VerifiedOpenId4VcAuthorizationResponse { idTokenPayload: IDTokenPayload - submission: VerifiedOpenID4VPSubmission | undefined + presentationExchange: + | { + submission: DifPresentationExchangeSubmission + definitions: DifPresentationExchangeDefinition[] + presentations: Array + } + | undefined } export type OpenId4VcAuthorizationResponse = AuthorizationResponsePayload diff --git a/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts b/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts index 552a868471..5ca5b64f7c 100644 --- a/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts @@ -17,17 +17,20 @@ export interface AuthorizationEndpointConfig { export function configureAuthorizationEndpoint(router: Router, config: AuthorizationEndpointConfig) { router.post(config.endpointPath, async (request: OpenId4VcVerificationRequest, response: Response) => { - const { agentContext } = getRequestContext(request) + const { agentContext, verifier } = getRequestContext(request) try { const openId4VcVerifierService = agentContext.dependencyManager.resolve(OpenId4VcVerifierService) const isVpRequest = request.body.presentation_submission !== undefined - // FIXME: body should be verified const authorizationResponse: AuthorizationResponsePayload = request.body if (isVpRequest) authorizationResponse.presentation_submission = JSON.parse(request.body.presentation_submission) - await openId4VcVerifierService.verifyAuthorizationResponse(agentContext, request.body) + // FIXME: we should emit an event here + await openId4VcVerifierService.verifyAuthorizationResponse(agentContext, { + authorizationResponse: request.body, + verifier, + }) return response.status(200).send() } catch (error) { return sendErrorResponse(response, agentContext.config.logger, 500, 'invalid_request', error) diff --git a/packages/openid4vc/tests/openid4vc.e2e.test.ts b/packages/openid4vc/tests/openid4vc.e2e.test.ts index 656cffade3..42763a28e0 100644 --- a/packages/openid4vc/tests/openid4vc.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc.e2e.test.ts @@ -1,5 +1,5 @@ import type { AgentType, TenantType } from './utils' -import type { CreateProofRequestOptions, CredentialBindingResolver } from '../src' +import type { CredentialBindingResolver } from '../src/openid4vc-holder' import type { SdJwtVc, SdJwtVcSignOptions } from '@aries-framework/sd-jwt-vc' import type { Server } from 'http' @@ -8,7 +8,6 @@ import { ClaimFormat, JwaSignatureAlgorithm, W3cCredential, - W3cCredentialService, W3cCredentialSubject, W3cIssuer, w3cDate, @@ -16,11 +15,12 @@ import { getKeyFromVerificationMethod, getJwkFromKey, AriesFrameworkError, + DifPresentationExchangeService, } from '@aries-framework/core' import { SdJwtVcModule } from '@aries-framework/sd-jwt-vc' import { TenantsModule } from '@aries-framework/tenants' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' -import express, { Router, type Express } from 'express' +import express, { type Express } from 'express' import { askarModuleConfig } from '../../askar/tests/helpers' import { OpenId4VcVerifierModule, OpenId4VcHolderModule, OpenId4VcIssuerModule } from '../src' @@ -31,116 +31,122 @@ import { openBadgePresentationDefinition, staticOpOpenIdConfigEdDSA, universityDegreePresentationDefinition, - waitForMockFunction, } from './utilsVp' -const issuerPort = 1234 -const baseUrl = `http://localhost:${issuerPort}/oid4vci` +const serverPort = 1234 +const baseUrl = `http://localhost:${serverPort}` +const issuanceBaseUrl = `${baseUrl}/oid4vci` +const verificationBaseUrl = `${baseUrl}/oid4vp` const baseCredentialOfferOptions = { scheme: 'openid-credential-offer', - baseUri: baseUrl, + baseUri: issuanceBaseUrl, } -const holderModules = { - openId4VcHolder: new OpenId4VcHolderModule(), - sdJwtVc: new SdJwtVcModule(), - askar: new AskarModule(askarModuleConfig), -} as const - -const oid4vciRouter = Router() -const issuerModules = { - openId4VcIssuer: new OpenId4VcIssuerModule({ - baseUrl, - router: oid4vciRouter, - endpoints: { - credential: { - // FIXME: should not be nested under the endpoint config, as it's also used for the non-endpoint part - credentialRequestToCredentialMapper: async ({ agentContext, credentialRequest, holderBinding }) => { - // We sign the request with the first did:key did we have - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - const [firstDidKeyDid] = await didsApi.getCreatedDids({ method: 'key' }) - const didDocument = await didsApi.resolveDidDocument(firstDidKeyDid.did) - const verificationMethod = didDocument.verificationMethod?.[0] - if (!verificationMethod) { - throw new Error('No verification method found') - } - - if (credentialRequest.format === 'vc+sd-jwt') { - return { - payload: { vct: credentialRequest.vct, university: 'innsbruck', degree: 'bachelor' }, - holder: holderBinding, - issuer: { - method: 'did', - didUrl: verificationMethod.id, - }, - disclosureFrame: { university: true, degree: true }, - } satisfies SdJwtVcSignOptions - } - - throw new Error('Invalid request') - }, - }, - }, - }), - sdJwtVc: new SdJwtVcModule(), - askar: new AskarModule(askarModuleConfig), -} as const - -const verifierModules = { - openId4VcVerifier: new OpenId4VcVerifierModule({ - verifierMetadata: { - verifierBaseUrl: baseUrl, - verificationEndpointPath: '/verify', - }, - }), - sdJwtVc: new SdJwtVcModule(), - askar: new AskarModule({ ariesAskar }), -} as const - describe('OpenId4Vc', () => { let expressApp: Express let expressServer: Server - let issuer: AgentType }> + let issuer: AgentType<{ + openId4VcIssuer: OpenId4VcIssuerModule + tenants: TenantsModule<{ openId4VcIssuer: OpenId4VcIssuerModule }> + }> let issuer1: TenantType let issuer2: TenantType - let holder: AgentType }> + let holder: AgentType<{ + openId4VcHolder: OpenId4VcHolderModule + sdJwtVc: SdJwtVcModule + tenants: TenantsModule<{ openId4VcHolder: OpenId4VcHolderModule; sdJwtVc: SdJwtVcModule }> + }> let holder1: TenantType - let verifier: AgentType }> + let verifier: AgentType<{ + openId4VcVerifier: OpenId4VcVerifierModule + tenants: TenantsModule<{ openId4VcVerifier: OpenId4VcVerifierModule }> + }> let verifier1: TenantType let verifier2: TenantType beforeEach(async () => { expressApp = express() - expressApp.use('/oid4vci', oid4vciRouter) - issuer = await createAgentFromModules( + issuer = (await createAgentFromModules( 'issuer', - { ...issuerModules, tenants: new TenantsModule() }, + { + openId4VcIssuer: new OpenId4VcIssuerModule({ + baseUrl: issuanceBaseUrl, + endpoints: { + credential: { + // FIXME: should not be nested under the endpoint config, as it's also used for the non-endpoint part + credentialRequestToCredentialMapper: async ({ agentContext, credentialRequest, holderBinding }) => { + // We sign the request with the first did:key did we have + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const [firstDidKeyDid] = await didsApi.getCreatedDids({ method: 'key' }) + const didDocument = await didsApi.resolveDidDocument(firstDidKeyDid.did) + const verificationMethod = didDocument.verificationMethod?.[0] + if (!verificationMethod) { + throw new Error('No verification method found') + } + + if (credentialRequest.format === 'vc+sd-jwt') { + return { + payload: { vct: credentialRequest.vct, university: 'innsbruck', degree: 'bachelor' }, + holder: holderBinding, + issuer: { + method: 'did', + didUrl: verificationMethod.id, + }, + disclosureFrame: { university: true, degree: true }, + } satisfies SdJwtVcSignOptions + } + + throw new Error('Invalid request') + }, + }, + }, + }), + sdJwtVc: new SdJwtVcModule(), + askar: new AskarModule(askarModuleConfig), + tenants: new TenantsModule(), + }, '96213c3d7fc8d4d6754c7a0fd969598g' - ) + )) as unknown as typeof issuer issuer1 = await createTenantForAgent(issuer.agent, 'iTenant1') issuer2 = await createTenantForAgent(issuer.agent, 'iTenant2') - holder = await createAgentFromModules( + holder = (await createAgentFromModules( 'holder', - { ...holderModules, tenants: new TenantsModule() }, + { + openId4VcHolder: new OpenId4VcHolderModule(), + sdJwtVc: new SdJwtVcModule(), + askar: new AskarModule(askarModuleConfig), + tenants: new TenantsModule(), + }, '96213c3d7fc8d4d6754c7a0fd969598e' - ) + )) as unknown as typeof holder holder1 = await createTenantForAgent(holder.agent, 'hTenant1') - verifier = await createAgentFromModules( + verifier = (await createAgentFromModules( 'verifier', - { ...verifierModules, tenants: new TenantsModule() }, + { + openId4VcVerifier: new OpenId4VcVerifierModule({ + baseUrl: verificationBaseUrl, + }), + sdJwtVc: new SdJwtVcModule(), + askar: new AskarModule({ ariesAskar }), + tenants: new TenantsModule(), + }, '96213c3d7fc8d4d6754c7a0fd969598f' - ) + )) as unknown as typeof verifier verifier1 = await createTenantForAgent(verifier.agent, 'vTenant1') verifier2 = await createTenantForAgent(verifier.agent, 'vTenant2') - expressServer = expressApp.listen(issuerPort) + // We let AFJ create the router, so we have a fresh one each time + expressApp.use('/oid4vci', issuer.agent.modules.openId4VcIssuer.config.router) + expressApp.use('/oid4vp', verifier.agent.modules.openId4VcVerifier.config.router) + + expressServer = expressApp.listen(serverPort) }) afterEach(async () => { @@ -170,8 +176,6 @@ describe('OpenId4Vc', () => { } } - console.log(supportsJwk, supportedDidMethods) - // otherwise throw an error throw new AriesFrameworkError('Issuer does not support did:key or JWK for credential binding') } @@ -212,13 +216,13 @@ describe('OpenId4Vc', () => { ) expect(resolvedCredentialOffer1.credentialOfferPayload.credential_issuer).toEqual( - `${baseUrl}/${openIdIssuerTenant1.issuerId}` + `${issuanceBaseUrl}/${openIdIssuerTenant1.issuerId}` ) expect(resolvedCredentialOffer1.metadata.credentialIssuerMetadata?.token_endpoint).toEqual( - `${baseUrl}/${openIdIssuerTenant1.issuerId}/token` + `${issuanceBaseUrl}/${openIdIssuerTenant1.issuerId}/token` ) expect(resolvedCredentialOffer1.metadata.credentialIssuerMetadata?.credential_endpoint).toEqual( - `${baseUrl}/${openIdIssuerTenant1.issuerId}/credential` + `${issuanceBaseUrl}/${openIdIssuerTenant1.issuerId}/credential` ) // Bind to JWK @@ -238,13 +242,13 @@ describe('OpenId4Vc', () => { credentialOffer2 ) expect(resolvedCredentialOffer2.credentialOfferPayload.credential_issuer).toEqual( - `${baseUrl}/${openIdIssuerTenant2.issuerId}` + `${issuanceBaseUrl}/${openIdIssuerTenant2.issuerId}` ) expect(resolvedCredentialOffer2.metadata.credentialIssuerMetadata?.token_endpoint).toEqual( - `${baseUrl}/${openIdIssuerTenant2.issuerId}/token` + `${issuanceBaseUrl}/${openIdIssuerTenant2.issuerId}/token` ) expect(resolvedCredentialOffer2.metadata.credentialIssuerMetadata?.credential_endpoint).toEqual( - `${baseUrl}/${openIdIssuerTenant2.issuerId}/credential` + `${issuanceBaseUrl}/${openIdIssuerTenant2.issuerId}/credential` ) // Bind to did @@ -263,200 +267,210 @@ describe('OpenId4Vc', () => { await holderTenant1.endSession() }) - xit('e2e flow with tenants, verifier endpoints verifying a sd-jwt-vc', async () => { - const mockFunction1 = jest.fn() - mockFunction1.mockReturnValue({ status: 200 }) - - const mockFunction2 = jest.fn() - mockFunction2.mockReturnValue({ status: 200 }) - - const issuerTenant1 = await issuer.agent.modules.tenants.getTenantAgent({ tenantId: issuer1.tenantId }) - const holderTenant1 = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId }) - const verifierTenant1_1 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) - const verifierTenant2_1 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier2.tenantId }) - const verifier1Router = Router() - const verifier2Router = Router() - const verifier1BasePath = '/verifier1' - const verifier2BasePath = '/verifier2' - - const credential1 = new W3cCredential({ - type: ['VerifiableCredential', 'OpenBadgeCredential'], - issuer: new W3cIssuer({ id: issuer1.did }), - credentialSubject: new W3cCredentialSubject({ id: holder1.did }), - issuanceDate: w3cDate(Date.now()), - }) - - const credential2 = new W3cCredential({ - type: ['VerifiableCredential', 'UniversityDegreeCredential'], - issuer: new W3cIssuer({ id: issuer1.did }), - credentialSubject: new W3cCredentialSubject({ id: holder1.did }), - issuanceDate: w3cDate(Date.now()), - }) + it('e2e flow with tenants, verifier endpoints verifying a sd-jwt-vc', async () => { + const holderTenant = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId }) + const verifierTenant1 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) + const verifierTenant2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier2.tenantId }) - const issuer1W3cCredentialService = issuerTenant1.dependencyManager.resolve(W3cCredentialService) + const openIdVerifierTenant1 = await verifierTenant1.modules.openId4VcVerifier.createVerifier() + const openIdVerifierTenant2 = await verifierTenant2.modules.openId4VcVerifier.createVerifier() - const signed1 = await issuer1W3cCredentialService.signCredential(issuerTenant1.context, { + const signedCredential1 = await issuer.agent.w3cCredentials.signCredential({ format: ClaimFormat.JwtVc, - credential: credential1, + credential: new W3cCredential({ + type: ['VerifiableCredential', 'OpenBadgeCredential'], + issuer: new W3cIssuer({ id: issuer.did }), + credentialSubject: new W3cCredentialSubject({ id: holder1.did }), + issuanceDate: w3cDate(Date.now()), + }), alg: JwaSignatureAlgorithm.EdDSA, - verificationMethod: issuer1.verificationMethod.id, + verificationMethod: issuer.verificationMethod.id, }) - const signed2 = await issuer1W3cCredentialService.signCredential(issuerTenant1.context, { + const signedCredential2 = await issuer.agent.w3cCredentials.signCredential({ format: ClaimFormat.JwtVc, - credential: credential2, + credential: new W3cCredential({ + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + issuer: new W3cIssuer({ id: issuer.did }), + credentialSubject: new W3cCredentialSubject({ id: holder1.did }), + issuanceDate: w3cDate(Date.now()), + }), alg: JwaSignatureAlgorithm.EdDSA, - verificationMethod: issuer1.verificationMethod.id, + verificationMethod: issuer.verificationMethod.id, }) - await holderTenant1.w3cCredentials.storeCredential({ credential: signed1 }) - await holderTenant1.w3cCredentials.storeCredential({ credential: signed2 }) - - await verifierTenant1_1.modules.openId4VcVerifier.configureRouter(verifier1Router, { - basePath: '/verifier1', - verificationEndpointConfig: { - enabled: true, - proofResponseHandler: mockFunction1, - }, - }) - - await verifierTenant2_1.modules.openId4VcVerifier.configureRouter(verifier2Router, { - basePath: '/verifier2', - verificationEndpointConfig: { - enabled: true, - proofResponseHandler: mockFunction2, - }, - }) - - expressApp.use(verifier1BasePath, verifier1Router) - expressApp.use(verifier2BasePath, verifier2Router) - expressServer = expressApp.listen(issuerPort) - - const createProofRequestOptions1: CreateProofRequestOptions = { - verificationMethod: verifier1.verificationMethod, - holderMetadata: staticOpOpenIdConfigEdDSA, - presentationDefinition: openBadgePresentationDefinition, - } - - const createProofRequestOptions2: CreateProofRequestOptions = { - verificationMethod: verifier2.verificationMethod, - holderMetadata: staticOpOpenIdConfigEdDSA, - presentationDefinition: universityDegreePresentationDefinition, - } + await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential1 }) + await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential2 }) - const { proofRequest: proofRequest1, proofRequestMetadata: proofRequestMetadata1 } = - await verifierTenant1_1.modules.openId4VcVerifier.createProofRequest(createProofRequestOptions1) + const { authorizationRequestUri: authorizationRequestUri1, metadata: proofRequestMetadata1 } = + await verifierTenant1.modules.openId4VcVerifier.createAuthorizationRequest({ + verificationMethod: verifier1.verificationMethod, + openIdProvider: staticOpOpenIdConfigEdDSA, + presentationDefinition: openBadgePresentationDefinition, + verifierId: openIdVerifierTenant1.verifierId, + }) + // FIXME: the presentation definition is in both top-level and request param? expect( - proofRequest1.startsWith( - `openid://?redirect_uri=http%3A%2F%2Flocalhost%3A1234%2Fverifier1%2Fverify&presentation_definition=%7B%22id%22%3A%22OpenBadgeCredential` + authorizationRequestUri1.startsWith( + `openid://?redirect_uri=http%3A%2F%2Flocalhost%3A1234%2Foid4vp%2F${openIdVerifierTenant1.verifierId}%2Fauthorize&presentation_definition=%7B%22id%22%3A%22OpenBadgeCredential` ) - ).toBeTruthy() - - const { proofRequest: proofRequest2, proofRequestMetadata: proofRequestMetadata2 } = - await verifierTenant2_1.modules.openId4VcVerifier.createProofRequest(createProofRequestOptions2) + ).toBe(true) + + const { authorizationRequestUri: authorizationRequestUri2, metadata: proofRequestMetadata2 } = + await verifierTenant2.modules.openId4VcVerifier.createAuthorizationRequest({ + verificationMethod: verifier2.verificationMethod, + openIdProvider: staticOpOpenIdConfigEdDSA, + presentationDefinition: universityDegreePresentationDefinition, + verifierId: openIdVerifierTenant2.verifierId, + }) + // FIXME: we should set scheme based on the openid provider metadata + // Is the op set in the request? + // FIXME: did:peer should not be supported expect( - proofRequest2.startsWith( - `openid://?redirect_uri=http%3A%2F%2Flocalhost%3A1234%2Fverifier2%2Fverify&presentation_definition=%7B%22id%22%3A%22UniversityDegreeCredential` + authorizationRequestUri2.startsWith( + `openid://?redirect_uri=http%3A%2F%2Flocalhost%3A1234%2Foid4vp%2F${openIdVerifierTenant2.verifierId}%2Fauthorize&presentation_definition=%7B%22id%22%3A%22UniversityDegreeCredential` ) - ).toBeTruthy() + ).toBe(true) - await verifierTenant1_1.endSession() - await verifierTenant2_1.endSession() + await verifierTenant1.endSession() + await verifierTenant2.endSession() - const result1 = await holderTenant1.modules.openId4VcHolder.resolveProofRequest(proofRequest1) - if (result1.proofType === 'authentication') throw new Error('Expected a proofRequest') - - result1.presentationSubmission.requirements[0] + // FIXME: api already has resolve authorization request + // but it's used for oid4vci. We should have some improvements on the api + const resolvedProofRequest1 = await holderTenant.modules.openId4VcHolder.resolveProofRequest( + authorizationRequestUri1 + ) + if (resolvedProofRequest1.proofType === 'authentication') throw new Error('Expected a proofRequest') - if (!result1.presentationSubmission.areRequirementsSatisfied) { + if (!resolvedProofRequest1.credentialsForRequest.areRequirementsSatisfied) { throw new Error('Requirements are not satisfied.') } expect( - result1.presentationSubmission.requirements[0].submissionEntry[0].verifiableCredentials[0].credential.type + resolvedProofRequest1.credentialsForRequest.requirements[0].submissionEntry[0].verifiableCredentials[0].credential + .type ).toContain('OpenBadgeCredential') - const result2 = await holderTenant1.modules.openId4VcHolder.resolveProofRequest(proofRequest2) - if (result2.proofType === 'authentication') throw new Error('Expected a proofRequest') - - result2.presentationSubmission.requirements[0] + const resolvedProofRequest2 = await holderTenant.modules.openId4VcHolder.resolveProofRequest( + authorizationRequestUri2 + ) + if (resolvedProofRequest2.proofType === 'authentication') throw new Error('Expected a proofRequest') - if (!result2.presentationSubmission.areRequirementsSatisfied) { + if (!resolvedProofRequest2.credentialsForRequest.areRequirementsSatisfied) { throw new Error('Requirements are not satisfied.') } + // FIXME: result MUST include SD-JWT as well expect( - result2.presentationSubmission.requirements[0].submissionEntry[0].verifiableCredentials[0].credential.type + resolvedProofRequest2.credentialsForRequest.requirements[0].submissionEntry[0].verifiableCredentials[0].credential + .type ).toContain('UniversityDegreeCredential') + const presentationExchangeService = holderTenant.dependencyManager.resolve(DifPresentationExchangeService) + const selectedCredentials = presentationExchangeService.selectCredentialsForRequest( + resolvedProofRequest1.credentialsForRequest + ) + const { status: status1, submittedResponse: submittedResponse1 } = - await holderTenant1.modules.openId4VcHolder.acceptPresentationRequest(result1.presentationRequest, { - submission: result1.presentationSubmission, - submissionEntryIndexes: [0], - }) + await holderTenant.modules.openId4VcHolder.acceptPresentationRequest( + resolvedProofRequest1.presentationRequest, + selectedCredentials + ) + expect(submittedResponse1).toEqual({ + expires_in: 6000, + id_token: expect.any(String), + presentation_submission: { + definition_id: 'OpenBadgeCredential', + descriptor_map: [ + { + format: 'jwt_vc', + id: 'OpenBadgeCredential', + path: '$.verifiableCredential[0]', + }, + ], + id: expect.any(String), + }, + state: expect.any(String), + vp_token: expect.any(String), + }) expect(status1).toBe(200) // The RP MUST validate that the aud (audience) Claim contains the value of the client_id // that the RP sent in the Authorization Request as an audience. // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. const verifierTenant1_2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) - const { idTokenPayload: idTokenPayload1, submission: submission1 } = - await verifierTenant1_2.modules.openId4VcVerifier.verifyProofResponse(submittedResponse1) - - const { state: state1, challenge: challenge1 } = proofRequestMetadata1 - expect(idTokenPayload1).toBeDefined() - expect(idTokenPayload1.state).toMatch(state1) - expect(idTokenPayload1.nonce).toMatch(challenge1) - - expect(submission1).toBeDefined() - expect(submission1?.presentationDefinitions).toHaveLength(1) - expect(submission1?.submissionData.definition_id).toBe('OpenBadgeCredential') - expect(submission1?.presentations).toHaveLength(1) - expect(submission1?.presentations[0].vcs[0].credential.type).toEqual([ - 'VerifiableCredential', - 'OpenBadgeCredential', - ]) - - await waitForMockFunction(mockFunction1) - expect(mockFunction1).toBeCalledWith({ - idTokenPayload: expect.objectContaining(idTokenPayload1), - submission: expect.objectContaining(submission1), + const { idTokenPayload: idTokenPayload1, presentationExchange: presentationExchange1 } = + await verifierTenant1_2.modules.openId4VcVerifier.verifyAuthorizationResponse({ + authorizationResponse: submittedResponse1, + verifierId: openIdVerifierTenant1.verifierId, + }) + + const { state: state1, nonce: nonce1 } = proofRequestMetadata1 + expect(idTokenPayload1).toMatchObject({ + state: state1, + nonce: nonce1, + }) + + expect(presentationExchange1).toMatchObject({ + definitions: [openBadgePresentationDefinition], + submission: { + definition_id: 'OpenBadgeCredential', + }, + presentations: [ + { + verifiableCredential: [ + { + type: ['VerifiableCredential', 'OpenBadgeCredential'], + }, + ], + }, + ], }) + const selectedCredentials2 = presentationExchangeService.selectCredentialsForRequest( + resolvedProofRequest2.credentialsForRequest + ) + + // FIXME: do we want to return the submitted response? And the status code? + // Also, do we get anything back for submitting this? const { status: status2, submittedResponse: submittedResponse2 } = - await holderTenant1.modules.openId4VcHolder.acceptPresentationRequest(result2.presentationRequest, { - submission: result2.presentationSubmission, - submissionEntryIndexes: [0], - }) + await holderTenant.modules.openId4VcHolder.acceptPresentationRequest( + resolvedProofRequest2.presentationRequest, + selectedCredentials2 + ) expect(status2).toBe(200) // The RP MUST validate that the aud (audience) Claim contains the value of the client_id // that the RP sent in the Authorization Request as an audience. // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. const verifierTenant2_2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier2.tenantId }) - const { idTokenPayload: idTokenPayload2, submission: submission2 } = - await verifierTenant2_2.modules.openId4VcVerifier.verifyProofResponse(submittedResponse2) - - const { state: state2, challenge: challenge2 } = proofRequestMetadata2 - expect(idTokenPayload2).toBeDefined() - expect(idTokenPayload2.state).toMatch(state2) - expect(idTokenPayload2.nonce).toMatch(challenge2) - - expect(submission2).toBeDefined() - expect(submission2?.presentationDefinitions).toHaveLength(1) - expect(submission2?.submissionData.definition_id).toBe('UniversityDegreeCredential') - expect(submission2?.presentations).toHaveLength(1) - expect(submission2?.presentations[0].vcs[0].credential.type).toEqual([ - 'VerifiableCredential', - 'UniversityDegreeCredential', - ]) - - await waitForMockFunction(mockFunction2) - expect(mockFunction2).toBeCalledWith({ - idTokenPayload: expect.objectContaining(idTokenPayload2), - submission: expect.objectContaining(submission2), + const { idTokenPayload: idTokenPayload2, presentationExchange: presentationExchange2 } = + await verifierTenant2_2.modules.openId4VcVerifier.verifyAuthorizationResponse({ + authorizationResponse: submittedResponse2, + verifierId: openIdVerifierTenant2.verifierId, + }) + + expect(idTokenPayload2).toMatchObject({ + state: proofRequestMetadata2.state, + nonce: proofRequestMetadata2.nonce, + }) + + expect(presentationExchange2).toMatchObject({ + definitions: [universityDegreePresentationDefinition], + submission: { + definition_id: 'UniversityDegreeCredential', + }, + presentations: [ + { + verifiableCredential: [ + { + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + }, + ], + }, + ], }) }) }) diff --git a/packages/openid4vc/tests/utilsVp.ts b/packages/openid4vc/tests/utilsVp.ts index 6d654aa026..67956b066d 100644 --- a/packages/openid4vc/tests/utilsVp.ts +++ b/packages/openid4vc/tests/utilsVp.ts @@ -13,8 +13,10 @@ import { } from '@aries-framework/core' import { SigningAlgo } from '@sphereon/did-auth-siop' -import { staticOpOpenIdConfig } from '../src' -import { staticOpSiopConfig } from '../src/openid4vc-verifier/OpenId4VcVerifierServiceOptions' +import { + openidStaticOpConfiguration, + siopv2StaticOpConfiguration, +} from '../src/openid4vc-verifier/staticOpConfiguration' import { getProofTypeFromKey } from '../src/shared/utils' export const waltPortalOpenBadgeJwt = @@ -108,19 +110,19 @@ export const combinePresentationDefinitions = ( } } -export const staticOpOpenIdConfigEdDSA: HolderMetadata = { - ...staticOpOpenIdConfig, +export const staticOpOpenIdConfigEdDSA = { + ...openidStaticOpConfiguration, idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA], requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA], vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] }, jwt_vp: { alg: [SigningAlgo.EDDSA] } }, -} +} satisfies HolderMetadata -export const staticSiopConfigEDDSA: HolderMetadata = { - ...staticOpSiopConfig, +export const staticSiopConfigEDDSA = { + ...siopv2StaticOpConfiguration, idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA], requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA], vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] }, jwt_vp: { alg: [SigningAlgo.EDDSA] } }, -} +} satisfies HolderMetadata // eslint-disable-next-line @typescript-eslint/no-explicit-any export function waitForMockFunction(mockFn: jest.Mock) { From 54b3a17c9f024ed1b31c70ca40b246402d2c6ae3 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Tue, 23 Jan 2024 16:08:24 +0700 Subject: [PATCH 106/115] add tags to sd-jwt record Signed-off-by: Timo Glastra --- .../sd-jwt-vc/src/repository/SdJwtVcRecord.ts | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/packages/sd-jwt-vc/src/repository/SdJwtVcRecord.ts b/packages/sd-jwt-vc/src/repository/SdJwtVcRecord.ts index 50f43635ee..13c6e93ff8 100644 --- a/packages/sd-jwt-vc/src/repository/SdJwtVcRecord.ts +++ b/packages/sd-jwt-vc/src/repository/SdJwtVcRecord.ts @@ -1,11 +1,20 @@ -import type { TagsBase, Constructable } from '@aries-framework/core' -import type { Disclosure } from '@sd-jwt/core' +import type { TagsBase, Constructable, JwaSignatureAlgorithm } from '@aries-framework/core' import { JsonTransformer, BaseRecord, utils } from '@aries-framework/core' import { SdJwtVc } from '@sd-jwt/core' export type DefaultSdJwtVcRecordTags = { - disclosureKeys?: Array + vct: string + + /** + * The sdAlg is the alg used for creating digests for selective disclosures + */ + sdAlg: string + + /** + * The alg is the alg used to sign the SD-JWT + */ + alg: JwaSignatureAlgorithm } export type SdJwtVcRecordStorageProps = { @@ -24,7 +33,6 @@ export class SdJwtVcRecord extends BaseRecord { // TODO: should we also store the pretty claims so it's not needed to // re-calculate the hashes each time? I think for now it's fine to re-calculate - public constructor(props: SdJwtVcRecordStorageProps) { super() @@ -37,14 +45,13 @@ export class SdJwtVcRecord extends BaseRecord { } public getTags() { - const disclosures = SdJwtVc.fromCompact(this.compactSdJwtVc).disclosures - const disclosureKeys = disclosures - ?.filter((d): d is Disclosure & { decoded: [string, string, unknown] } => d.decoded.length === 3) - .map((d) => d.decoded[1]) + const sdJwtVc = SdJwtVc.fromCompact(this.compactSdJwtVc) return { ...this._tags, - disclosureKeys, + vct: sdJwtVc.getClaimInPayload('vct'), + sdAlg: (sdJwtVc.payload._sd_alg as string | undefined) ?? 'sha-256', + alg: sdJwtVc.getClaimInHeader('alg'), } } From 654bf1fe6167c30afbabcd479425b0a54a105d5a Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Tue, 23 Jan 2024 16:11:08 +0700 Subject: [PATCH 107/115] a lot of fixmes Signed-off-by: Timo Glastra --- packages/core/tests/index.ts | 4 +- .../openid4vc-holder/OpenId4VcHolderApi.ts | 22 +- .../OpenId4VciHolderService.ts | 62 ++- .../OpenId4VciHolderServiceOptions.ts | 34 +- .../OpenId4VpHolderService.ts | 14 +- .../OpenId4VpHolderServiceOptions.ts | 2 - .../openid4vc-issuer/OpenId4VcIssuerApi.ts | 4 +- .../openid4vc-issuer/OpenId4VcIssuerModule.ts | 15 +- .../OpenId4VcIssuerModuleConfig.ts | 3 - .../OpenId4VcIssuerService.ts | 53 +-- .../OpenId4VcIssuerServiceOptions.ts | 48 +- .../router/accessTokenEndpoint.ts | 34 +- .../router/credentialEndpoint.ts | 9 +- .../router/metadataEndpoint.ts | 40 +- .../OpenId4VcVerifierModule.ts | 11 +- .../OpenId4VcVerifierService.ts | 433 +++++++++--------- .../__tests__/openid4vc-verifier.e2e.test.ts | 20 +- .../repository/OpenId4VcVerifierRecord.ts | 1 - .../router/authorizationEndpoint.ts | 9 +- .../shared/models/CredentialHolderBinding.ts | 6 +- .../openid4vc/src/shared/router/tenants.ts | 1 - packages/openid4vc/src/shared/transform.ts | 92 ++-- .../openid4vc/tests/openid4vc.e2e.test.ts | 18 +- packages/openid4vc/tests/utils.ts | 13 +- 24 files changed, 477 insertions(+), 471 deletions(-) diff --git a/packages/core/tests/index.ts b/packages/core/tests/index.ts index 2822fb23e1..62d138bcde 100644 --- a/packages/core/tests/index.ts +++ b/packages/core/tests/index.ts @@ -4,6 +4,6 @@ export * from './events' export * from './helpers' export * from './indySdk' -import testLogger from './logger' +import testLogger, { TestLogger } from './logger' -export { testLogger } +export { testLogger, TestLogger } diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts index d58c1c17be..37f4913d61 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts @@ -1,8 +1,8 @@ import type { - ResolvedCredentialOffer, - ResolvedAuthorizationRequest, - AuthCodeFlowOptions, - AcceptCredentialOfferOptions, + OpenId4VciResolvedCredentialOffer, + OpenId4VciResolvedAuthorizationRequest, + OpenId4VciAuthCodeFlowOptions, + OpenId4VciAcceptCredentialOfferOptions, } from './OpenId4VciHolderServiceOptions' import type { AuthenticationRequest, PresentationRequest } from './OpenId4VpHolderServiceOptions' import type { VerificationMethod, DifPexInputDescriptorToCredentials } from '@aries-framework/core' @@ -105,8 +105,8 @@ export class OpenId4VcHolderApi { * @returns The authorization request URI alongside the code verifier and original @param authCodeFlowOptions */ public async resolveAuthorizationRequest( - resolvedCredentialOffer: ResolvedCredentialOffer, - authCodeFlowOptions: AuthCodeFlowOptions + resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer, + authCodeFlowOptions: OpenId4VciAuthCodeFlowOptions ) { return await this.openId4VciHolderService.resolveAuthorizationRequest( this.agentContext, @@ -122,8 +122,8 @@ export class OpenId4VcHolderApi { * @returns ( @see W3cCredentialRecord | @see SdJwtRecord )[] */ public async acceptCredentialOfferUsingPreAuthorizedCode( - resolvedCredentialOffer: ResolvedCredentialOffer, - acceptCredentialOfferOptions: AcceptCredentialOfferOptions + resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer, + acceptCredentialOfferOptions: OpenId4VciAcceptCredentialOfferOptions ) { return this.openId4VciHolderService.acceptCredentialOffer(this.agentContext, { resolvedCredentialOffer, @@ -140,10 +140,10 @@ export class OpenId4VcHolderApi { * @returns ( @see W3cCredentialRecord | @see SdJwtRecord )[] */ public async acceptCredentialOfferUsingAuthorizationCode( - resolvedCredentialOffer: ResolvedCredentialOffer, - resolvedAuthorizationRequest: ResolvedAuthorizationRequest, + resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer, + resolvedAuthorizationRequest: OpenId4VciResolvedAuthorizationRequest, code: string, - acceptCredentialOfferOptions: AcceptCredentialOfferOptions + acceptCredentialOfferOptions: OpenId4VciAcceptCredentialOfferOptions ) { return this.openId4VciHolderService.acceptCredentialOffer(this.agentContext, { resolvedCredentialOffer, diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts index 273e8b66a3..0dda7f3254 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts @@ -57,21 +57,21 @@ import { import { getSupportedJwaSignatureAlgorithms } from '../shared/utils' import { - type AuthCodeFlowOptions, - type AcceptCredentialOfferOptions, - type ProofOfPossessionRequirements, - type CredentialBindingResolver, - type ResolvedCredentialOffer, - type ResolvedAuthorizationRequest, - type ResolvedAuthorizationRequestWithCode, - type SupportedCredentialFormats, - supportedCredentialFormats, + type OpenId4VciAuthCodeFlowOptions, + type OpenId4VciAcceptCredentialOfferOptions, + type OpenId4VciProofOfPossessionRequirements, + type OpenId4VciCredentialBindingResolver, + type OpenId4VciResolvedCredentialOffer, + type OpenId4VciResolvedAuthorizationRequest, + type OpenId4VciResolvedAuthorizationRequestWithCode, + type OpenId4VciSupportedCredentialFormats, + openId4VciSupportedCredentialFormats, } from './OpenId4VciHolderServiceOptions' // FIXME: this is also defined in the sphereon lib, is there a reason we don't use that one? async function createAuthorizationRequestUri(options: { credentialOffer: OpenId4VciCredentialOfferPayload - metadata: ResolvedCredentialOffer['metadata'] + metadata: OpenId4VciResolvedCredentialOffer['metadata'] clientId: string codeChallenge: string codeChallengeMethod: CodeChallengeMethod @@ -161,7 +161,7 @@ export class OpenId4VciHolderService { this.logger = logger } - public async resolveCredentialOffer(credentialOffer: string): Promise { + public async resolveCredentialOffer(credentialOffer: string): Promise { const client = await OpenID4VCIClient.fromURI({ uri: credentialOffer, resolveOfferUri: true, @@ -240,9 +240,9 @@ export class OpenId4VciHolderService { // need to make sure difference is clear public async resolveAuthorizationRequest( agentContext: AgentContext, - resolvedCredentialOffer: ResolvedCredentialOffer, - authCodeFlowOptions: AuthCodeFlowOptions - ): Promise { + resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer, + authCodeFlowOptions: OpenId4VciAuthCodeFlowOptions + ): Promise { const { credentialOfferPayload, metadata, offeredCredentials } = resolvedCredentialOffer const codeVerifier = `${await agentContext.wallet.generateNonce()}${await agentContext.wallet.generateNonce()}` const codeVerifierSha256 = Hasher.hash(TypedArrayEncoder.fromString(codeVerifier), 'sha2-256') @@ -285,9 +285,9 @@ export class OpenId4VciHolderService { public async acceptCredentialOffer( agentContext: AgentContext, options: { - resolvedCredentialOffer: ResolvedCredentialOffer - acceptCredentialOfferOptions: AcceptCredentialOfferOptions - resolvedAuthorizationRequestWithCode?: ResolvedAuthorizationRequestWithCode + resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer + acceptCredentialOfferOptions: OpenId4VciAcceptCredentialOfferOptions + resolvedAuthorizationRequestWithCode?: OpenId4VciResolvedAuthorizationRequestWithCode } ) { const { resolvedCredentialOffer, acceptCredentialOfferOptions, resolvedAuthorizationRequestWithCode } = options @@ -354,7 +354,21 @@ export class OpenId4VciHolderService { const receivedCredentials: Array = [] let newCNonce: string | undefined - for (const offeredCredential of credentialsToRequest ?? offeredCredentials) { + const credentialsSupportedToRequest = + credentialsToRequest + ?.map((id) => offeredCredentials.find((credential) => credential.id === id)) + .filter((c, i): c is OpenId4VciCredentialSupportedWithId => { + if (!c) { + const offeredCredentialIds = offeredCredentials.map((c) => c.id).join(', ') + throw new AriesFrameworkError( + `Credential to request '${credentialsToRequest[i]}' is not present in offered credentials. Offered credentials are ${offeredCredentialIds}` + ) + } + + return true + }) ?? offeredCredentials + + for (const offeredCredential of credentialsSupportedToRequest) { // Get all options for the credential request (such as which kid to use, the signature algorithm, etc) const { credentialBinding, signatureAlgorithm } = await this.getCredentialRequestOptions(agentContext, { possibleProofOfPossessionSignatureAlgorithms: possibleProofOfPossessionSigAlgs, @@ -419,7 +433,7 @@ export class OpenId4VciHolderService { private async getCredentialRequestOptions( agentContext: AgentContext, options: { - credentialBindingResolver: CredentialBindingResolver + credentialBindingResolver: OpenId4VciCredentialBindingResolver possibleProofOfPossessionSignatureAlgorithms: JwaSignatureAlgorithm[] offeredCredential: OpenId4VciCredentialSupportedWithId } @@ -439,7 +453,7 @@ export class OpenId4VciHolderService { const supportedVerificationMethods = getSupportedVerificationMethodTypesFromKeyType(JwkClass.keyType) - const format = options.offeredCredential.format as SupportedCredentialFormats + const format = options.offeredCredential.format as OpenId4VciSupportedCredentialFormats // Now we need to determine how the credential will be bound to us const credentialBinding = await options.credentialBindingResolver({ @@ -497,15 +511,17 @@ export class OpenId4VciHolderService { credentialToRequest: OpenId4VciCredentialSupportedWithId possibleProofOfPossessionSignatureAlgorithms: JwaSignatureAlgorithm[] } - ): ProofOfPossessionRequirements { + ): OpenId4VciProofOfPossessionRequirements { const { credentialToRequest } = options - if (!supportedCredentialFormats.includes(credentialToRequest.format as SupportedCredentialFormats)) { + if ( + !openId4VciSupportedCredentialFormats.includes(credentialToRequest.format as OpenId4VciSupportedCredentialFormats) + ) { throw new AriesFrameworkError( [ `Requested credential with format '${credentialToRequest.format}',`, `for the credential with id '${credentialToRequest.id},`, - `but the wallet only supports the following formats '${supportedCredentialFormats.join(', ')}'`, + `but the wallet only supports the following formats '${openId4VciSupportedCredentialFormats.join(', ')}'`, ].join('\n') ) } diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderServiceOptions.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderServiceOptions.ts index 0369a6260e..a3756ff771 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderServiceOptions.ts @@ -1,5 +1,5 @@ import type { - CredentialHolderBinding, + OpenId4VcCredentialHolderBinding, OpenId4VciCredentialOfferPayload, OpenId4VciCredentialSupportedWithId, OpenId4VciIssuerMetadata, @@ -9,20 +9,20 @@ import type { AuthorizationServerMetadata, EndpointMetadataResult, OpenId4VCIVer import { OpenId4VciCredentialFormatProfile } from '../shared/models/OpenId4VciCredentialFormatProfile' -export type SupportedCredentialFormats = +export type OpenId4VciSupportedCredentialFormats = | OpenId4VciCredentialFormatProfile.JwtVcJson | OpenId4VciCredentialFormatProfile.JwtVcJsonLd | OpenId4VciCredentialFormatProfile.SdJwtVc | OpenId4VciCredentialFormatProfile.LdpVc -export const supportedCredentialFormats: SupportedCredentialFormats[] = [ +export const openId4VciSupportedCredentialFormats: OpenId4VciSupportedCredentialFormats[] = [ OpenId4VciCredentialFormatProfile.JwtVcJson, OpenId4VciCredentialFormatProfile.JwtVcJsonLd, OpenId4VciCredentialFormatProfile.SdJwtVc, OpenId4VciCredentialFormatProfile.LdpVc, ] -export interface ResolvedCredentialOffer { +export interface OpenId4VciResolvedCredentialOffer { metadata: EndpointMetadataResult & { credentialIssuerMetadata: Partial & OpenId4VciIssuerMetadata } @@ -31,19 +31,19 @@ export interface ResolvedCredentialOffer { version: OpenId4VCIVersion } -export interface ResolvedAuthorizationRequest extends AuthCodeFlowOptions { +export interface OpenId4VciResolvedAuthorizationRequest extends OpenId4VciAuthCodeFlowOptions { codeVerifier: string authorizationRequestUri: string } -export interface ResolvedAuthorizationRequestWithCode extends ResolvedAuthorizationRequest { +export interface OpenId4VciResolvedAuthorizationRequestWithCode extends OpenId4VciResolvedAuthorizationRequest { code: string } /** * Options that are used to accept a credential offer for both the pre-authorized code flow and authorization code flow. */ -export interface AcceptCredentialOfferOptions { +export interface OpenId4VciAcceptCredentialOfferOptions { /** * String value containing a user PIN. This value MUST be present if user_pin_required was set to true in the Credential Offer. * This parameter MUST only be used, if the grant_type is urn:ietf:params:oauth:grant-type:pre-authorized_code. @@ -52,10 +52,10 @@ export interface AcceptCredentialOfferOptions { /** * This is the list of credentials that will be requested from the issuer. + * Should be a list of ids of the credentials that are included in the credential offer. * If not provided all offered credentials will be requested. - * FIXME: should this be an list of ids? */ - credentialsToRequest?: OpenId4VciCredentialSupportedWithId[] + credentialsToRequest?: string[] verifyCredentialStatus?: boolean @@ -86,25 +86,25 @@ export interface AcceptCredentialOfferOptions { * conformant to the `CredentialHolderBinding` interface, which will be used * for the proof of possession signature. */ - credentialBindingResolver: CredentialBindingResolver + credentialBindingResolver: OpenId4VciCredentialBindingResolver } /** * Options that are used for the authorization code flow. * Extends the pre-authorized code flow options. */ -export interface AuthCodeFlowOptions { +export interface OpenId4VciAuthCodeFlowOptions { clientId: string redirectUri: string scope?: string[] } -export interface CredentialBindingOptions { +export interface OpenId4VciCredentialBindingOptions { /** * The credential format that will be requested from the issuer. * E.g. `jwt_vc` or `ldp_vc`. */ - credentialFormat: SupportedCredentialFormats + credentialFormat: OpenId4VciSupportedCredentialFormats /** * The JWA Signature Algorithm that will be used in the proof of possession. @@ -176,14 +176,14 @@ export interface CredentialBindingOptions { * user of the framework and allows them to determine which verification method should be used * for the proof of possession signature. */ -export type CredentialBindingResolver = ( - options: CredentialBindingOptions -) => Promise | CredentialHolderBinding +export type OpenId4VciCredentialBindingResolver = ( + options: OpenId4VciCredentialBindingOptions +) => Promise | OpenId4VcCredentialHolderBinding /** * @internal */ -export interface ProofOfPossessionRequirements { +export interface OpenId4VciProofOfPossessionRequirements { signatureAlgorithm: JwaSignatureAlgorithm supportedDidMethods?: string[] supportsAllDidMethods: boolean diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderService.ts index 8f852069bb..1f924c9f8e 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderService.ts @@ -173,11 +173,23 @@ export class OpenId4VpHolderService { presentationRequest: PresentationRequest, credentialsForInputDescriptor: DifPexInputDescriptorToCredentials ): Promise { + // FIXME: make sure nonce and clientId are also verified in the verify proof method + const nonce = await presentationRequest.authorizationRequest.getMergedProperty('nonce') + if (!nonce) { + throw new AriesFrameworkError("Unable to extract 'nonce' from authorization request") + } + + const clientId = await presentationRequest.authorizationRequest.getMergedProperty('client_id') + if (!clientId) { + throw new AriesFrameworkError("Unable to extract 'client_id' from authorization request") + } + const { verifiablePresentations, presentationSubmission } = await this.presentationExchangeService.createPresentation(agentContext, { credentialsForInputDescriptor, presentationDefinition: presentationRequest.presentationDefinitions[0].definition, - nonce: await presentationRequest.authorizationRequest.getMergedProperty('nonce'), + challenge: nonce, + domain: clientId, }) const verificationMethod = await this.getVerificationMethodFromVerifiablePresentation( diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderServiceOptions.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderServiceOptions.ts index 11ab21b1d5..175dc38772 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderServiceOptions.ts @@ -39,5 +39,3 @@ export type ProofSubmissionResponse = { status: number submittedResponse: AuthorizationResponsePayload } - -export type VpFormat = 'jwt_vp' | 'ldp_vp' diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts index 81d9ba29b9..7ddd1c2f12 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts @@ -1,5 +1,5 @@ import type { - CreateCredentialResponseOptions, + OpenId4VciCreateCredentialResponseOptions, OpenId4VciCreateCredentialOfferOptions, CredentialOffer, } from './OpenId4VcIssuerServiceOptions' @@ -98,7 +98,7 @@ export class OpenId4VcIssuerApi { * @param options.credential - The credential to be issued. * @param options.verificationMethod - The verification method used for signing the credential. */ - public async createCredentialResponse(options: CreateCredentialResponseOptions & { issuerId: string }) { + public async createCredentialResponse(options: OpenId4VciCreateCredentialResponseOptions & { issuerId: string }) { const { issuerId, ...rest } = options const issuer = await this.openId4VcIssuerService.getByIssuerId(this.agentContext, issuerId) return await this.openId4VcIssuerService.createCredentialResponse(this.agentContext, { ...rest, issuer }) diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts index 3ff05b75a4..f986603b71 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModule.ts @@ -57,7 +57,7 @@ export class OpenId4VcIssuerModule implements Module { private configureRouter(rootAgentContext: AgentContext) { const { Router, json, urlencoded } = importExpress() - // FIXME: it is currently not possible to initialize an agent + // TODO: it is currently not possible to initialize an agent // shut it down, and then start it again, as the // express router is configured with a specific `AgentContext` instance // and dependency manager. One option is to always create a new router @@ -101,6 +101,7 @@ export class OpenId4VcIssuerModule implements Module { ) // If the opening failed await agentContext?.endSession() + return _res.status(404).send('Not found') } @@ -114,8 +115,16 @@ export class OpenId4VcIssuerModule implements Module { configureAccessTokenEndpoint(endpointRouter, this.config.accessTokenEndpoint) configureCredentialEndpoint(endpointRouter, this.config.credentialEndpoint) - // FIXME: Will this be called when an error occurs / 404 is returned earlier on? - contextRouter.use(async (req: OpenId4VcIssuanceRequest, _res, next) => { + // First one will be called for all requests (when next is called) + contextRouter.use(async (req: OpenId4VcIssuanceRequest, _res: unknown, next) => { + const { agentContext } = getRequestContext(req) + await agentContext.endSession() + next() + }) + + // This one will be called for all errors that are thrown + // eslint-disable-next-line @typescript-eslint/no-explicit-any + contextRouter.use(async (_error: unknown, req: OpenId4VcIssuanceRequest, _res: unknown, next: any) => { const { agentContext } = getRequestContext(req) await agentContext.endSession() next() diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts index 71157cbe0e..c2ec8fc0dd 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts @@ -30,8 +30,6 @@ export interface OpenId4VcIssuerModuleConfigOptions { router?: Router endpoints: { - // metadata endpoint does not have a config - // metadata?: MetadataEndpointConfig credential: Optional accessToken?: Optional< AccessTokenEndpointConfig, @@ -49,7 +47,6 @@ export class OpenId4VcIssuerModuleConfig { private options: OpenId4VcIssuerModuleConfigOptions public readonly router: Router - // FIXME: remove private credentialOfferSessionManagerMap: Map> private uriStateManagerMap: Map> private cNonceStateManagerMap: Map> diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts index a2f8dc1b87..6c621c4f4e 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts @@ -1,17 +1,15 @@ import type { - CreateCredentialResponseOptions, - CredentialOffer, + OpenId4VciCreateCredentialResponseOptions, OpenId4VciAuthorizationCodeFlowConfig, OpenId4VciCreateCredentialOfferOptions, OpenId4VciCreateIssuerOptions, OpenId4VciPreAuthorizedCodeFlowConfig, - OpenId4VciSignCredential, + OpenId4VcIssuerMetadata, OpenId4VciSignSdJwtCredential, OpenId4VciSignW3cCredential, - OpenId4VcIssuerMetadata, } from './OpenId4VcIssuerServiceOptions' import type { - CredentialHolderBinding, + OpenId4VcCredentialHolderBinding, OpenId4VciCredentialOfferPayload, OpenId4VciCredentialRequest, OpenId4VciCredentialSupported, @@ -44,7 +42,6 @@ import { Jwt, KeyType, utils, - W3cCredential, W3cCredentialService, } from '@aries-framework/core' import { IssueStatus } from '@sphereon/oid4vci-common' @@ -52,7 +49,7 @@ import { VcIssuerBuilder } from '@sphereon/oid4vci-issuer' import { getOfferedCredentials, OpenId4VciCredentialFormatProfile } from '../shared' import { storeActorIdForContextCorrelationId } from '../shared/router' -import { getSphereonW3cVerifiableCredential } from '../shared/transform' +import { getSphereonVerifiableCredential } from '../shared/transform' import { getProofTypeFromKey } from '../shared/utils' import { OpenId4VcIssuerModuleConfig } from './OpenId4VcIssuerModuleConfig' @@ -104,7 +101,7 @@ export class OpenId4VcIssuerService { public async createCredentialOffer( agentContext: AgentContext, options: OpenId4VciCreateCredentialOfferOptions & { issuer: OpenId4VcIssuerRecord } - ): Promise { + ) { const { preAuthorizedCodeFlowConfig, authorizationCodeFlowConfig, issuer, offeredCredentials } = options const vcIssuer = this.getVcIssuer(agentContext, issuer) @@ -116,14 +113,14 @@ export class OpenId4VcIssuerService { const { uri, session } = await vcIssuer.createCredentialOfferURI({ grants: await this.getGrantsFromConfig(agentContext, preAuthorizedCodeFlowConfig, authorizationCodeFlowConfig), credentials: offeredCredentials, - credentialOfferUri: options.credentialOfferUri, - scheme: options.scheme ?? 'https', + credentialOfferUri: options.hostedCredentialOfferUrl, baseUri: options.baseUri, }) + const credentialOfferPayload: OpenId4VciCredentialOfferPayload = session.credentialOffer.credential_offer return { - credentialOfferPayload: session.credentialOffer.credential_offer, - credentialOfferUri: uri, + credentialOfferPayload, + credentialOffer: uri, } } @@ -144,7 +141,7 @@ export class OpenId4VcIssuerService { public async createCredentialResponse( agentContext: AgentContext, - options: CreateCredentialResponseOptions & { issuer: OpenId4VcIssuerRecord } + options: OpenId4VciCreateCredentialResponseOptions & { issuer: OpenId4VcIssuerRecord } ) { const { credentialRequest, issuer } = options if (!credentialRequest.proof) throw new AriesFrameworkError('No proof defined in the credentialRequest.') @@ -152,7 +149,6 @@ export class OpenId4VcIssuerService { const vcIssuer = this.getVcIssuer(agentContext, issuer) const issueCredentialResponse = await vcIssuer.issueCredential({ credentialRequest, - // FIXME: move this to top-level config (or at least not endpoint config) tokenExpiresIn: this.openId4VcIssuerConfig.accessTokenEndpoint.tokenExpiresInSeconds, // This can just be combined with signing callback right? @@ -255,7 +251,7 @@ export class OpenId4VcIssuerService { if (!isValid) throw new AriesFrameworkError('Could not verify JWT signature.') - // FIXME: the jws service should return some better decoded metadata also from the resolver + // TODO: the jws service should return some better decoded metadata also from the resolver // as currently is less useful if you afterwards need properties from the JWS const firstJws = jws.signatures[0] const protectedHeader = JsonEncoder.fromBase64(firstJws.protected) @@ -278,7 +274,6 @@ export class OpenId4VcIssuerService { .withCredentialEndpoint(issuerMetadata.credentialEndpoint) .withTokenEndpoint(issuerMetadata.tokenEndpoint) .withCredentialsSupported(issuerMetadata.credentialsSupported) - // FIXME: need to create persistent state managers .withCNonceStateManager(this.openId4VcIssuerConfig.getCNonceStateManager(agentContext)) .withCredentialOfferStateManager(this.openId4VcIssuerConfig.getCredentialOfferSessionStateManager(agentContext)) .withCredentialOfferURIStateManager(this.openId4VcIssuerConfig.getUriStateManager(agentContext)) @@ -368,9 +363,8 @@ export class OpenId4VcIssuerService { const sdJwtVcApi = getApiForModuleByName(agentContext, 'SdJwtVcModule') if (!sdJwtVcApi) throw new AriesFrameworkError(`Could not find the SdJwtVcApi`) - const { compact } = await sdJwtVcApi.sign(options) - - return compact + const sdJwtVc = await sdJwtVcApi.sign(options) + return getSphereonVerifiableCredential(sdJwtVc) } } @@ -421,7 +415,7 @@ export class OpenId4VcIssuerService { alg, }) - return getSphereonW3cVerifiableCredential(signed) + return getSphereonVerifiableCredential(signed) } else { const key = getKeyFromVerificationMethod(verificationMethod) const proofType = getProofTypeFromKey(agentContext, key) @@ -433,7 +427,7 @@ export class OpenId4VcIssuerService { proofType: proofType, }) - return getSphereonW3cVerifiableCredential(signed) + return getSphereonVerifiableCredential(signed) } } } @@ -455,12 +449,12 @@ export class OpenId4VcIssuerService { return { method: 'did', didUrl: jwt.header.kid, - } satisfies CredentialHolderBinding + } satisfies OpenId4VcCredentialHolderBinding } else if (jwt.header.jwk) { return { method: 'jwk', jwk: getJwkFromJson(jwt.header.jwk), - } satisfies CredentialHolderBinding + } satisfies OpenId4VcCredentialHolderBinding } else { throw new AriesFrameworkError('Either kid or jwk must be present in credential request proof header') } @@ -468,7 +462,7 @@ export class OpenId4VcIssuerService { private getCredentialDataSupplier = ( agentContext: AgentContext, - options: CreateCredentialResponseOptions & { issuer: OpenId4VcIssuerRecord } + options: OpenId4VciCreateCredentialResponseOptions & { issuer: OpenId4VcIssuerRecord } ): CredentialDataSupplier => { return async (args: CredentialDataSupplierArgs) => { const { credentialRequest, credentialOffer } = args @@ -504,7 +498,7 @@ export class OpenId4VcIssuerService { credentialsSupported: offeredCredentialsMatchingRequest, }) - if (isW3cSignCredentialOptions(signOptions)) { + if (signOptions.format === ClaimFormat.JwtVc || signOptions.format === ClaimFormat.LdpVc) { if (!w3cOpenId4VcFormats.includes(credentialRequest.format as OpenId4VciCredentialFormatProfile)) { throw new AriesFrameworkError( `The credential to be issued does not match the request. Cannot issue a W3cCredential if the client expects a credential of format '${credentialRequest.format}'.` @@ -516,7 +510,7 @@ export class OpenId4VcIssuerService { credential: JsonTransformer.toJSON(signOptions.credential) as ICredential, signCallback: this.getW3cCredentialSigningCallback(agentContext, signOptions), } - } else { + } else if (signOptions.format === ClaimFormat.SdJwtVc) { if (credentialRequest.format !== OpenId4VciCredentialFormatProfile.SdJwtVc) { throw new AriesFrameworkError( `Invalid credential format. Expected '${OpenId4VciCredentialFormatProfile.SdJwtVc}', received '${credentialRequest.format}'.` @@ -531,15 +525,12 @@ export class OpenId4VcIssuerService { return { format: credentialRequest.format, // NOTE: we don't use the credential value here as we pass the credential directly to the singer - // FIXME: oid4vci adds `sub` property, but SD-JWT uses `cnf` credential: { ...signOptions.payload } as unknown as CredentialIssuanceInput, signCallback: this.getSdJwtVcCredentialSigningCallback(agentContext, signOptions), } + } else { + throw new AriesFrameworkError(`Unsupported credential format`) } } } } - -function isW3cSignCredentialOptions(credential: OpenId4VciSignCredential): credential is OpenId4VciSignW3cCredential { - return 'credential' in credential && credential.credential instanceof W3cCredential -} diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts index ce306725b3..eaa788fce1 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts @@ -1,13 +1,12 @@ import type { OpenId4VcIssuerRecordProps } from './repository' import type { - CredentialHolderBinding, + OpenId4VcCredentialHolderBinding, OpenId4VciCredentialOffer, - OpenId4VciCredentialOfferPayload, OpenId4VciCredentialRequest, OpenId4VciCredentialSupported, OpenId4VciIssuerMetadataDisplay, } from '../shared' -import type { AgentContext, W3cCredential } from '@aries-framework/core' +import type { AgentContext, ClaimFormat, W3cCredential } from '@aries-framework/core' import type { SdJwtVcSignOptions } from '@aries-framework/sd-jwt-vc' export interface OpenId4VciPreAuthorizedCodeFlowConfig { @@ -38,28 +37,24 @@ export interface OpenId4VciCreateCredentialOfferOptions { // we only support referenced credentials in an offer offeredCredentials: string[] - // FIXME: can we simplify this? - // The scheme used for the credentialIssuer. Default is https - scheme?: 'http' | 'https' | 'openid-credential-offer' | string - - // The base URI of the credential offer uri + /** + * baseUri for the credential offer uri. By default `openid-credential-offer://` will be used + * if no value is provided. If a value is provided, make sure it contains the scheme as well as `://`. + */ baseUri?: string preAuthorizedCodeFlowConfig?: OpenId4VciPreAuthorizedCodeFlowConfig authorizationCodeFlowConfig?: OpenId4VciAuthorizationCodeFlowConfig - credentialOfferUri?: string -} - -// FIXME: this needs to be renamed, but will class with OpenId4VciCredentialOffer -// Probably needs to be specific `XXReturn` type -export type CredentialOffer = { - credentialOfferPayload: OpenId4VciCredentialOfferPayload - credentialOfferUri: string + /** + * You can provide a `hostedCredentialOfferUrl` if the created credential offer + * should points to a hosted credential offer in the `credential_offer_uri` field + * of the credential offer. + */ + hostedCredentialOfferUrl?: string } -// FIXME: openid4vc prefix for all interfaces -export interface CreateCredentialResponseOptions { +export interface OpenId4VciCreateCredentialResponseOptions { credentialRequest: OpenId4VciCredentialRequest /** @@ -72,9 +67,9 @@ export interface CreateCredentialResponseOptions { credentialRequestToCredentialMapper?: OpenId4VciCredentialRequestToCredentialMapper } -// FIXME: openid4vc prefix for all interfaces // FIXME: Flows: // - provide credential data at time of offer creation +// - provide credential data at time of calling createCredentialResponse // - provide credential data dynamically using this method export type OpenId4VciCredentialRequestToCredentialMapper = (options: { agentContext: AgentContext @@ -94,7 +89,7 @@ export type OpenId4VciCredentialRequestToCredentialMapper = (options: { * * Can either be bound to did or a JWK (in case of for ex. SD-JWT) */ - holderBinding: CredentialHolderBinding + holderBinding: OpenId4VcCredentialHolderBinding /** * The credentials supported entries from the issuer metadata that were offered @@ -105,16 +100,13 @@ export type OpenId4VciCredentialRequestToCredentialMapper = (options: { credentialsSupported: OpenId4VciCredentialSupported[] }) => Promise | OpenId4VciSignCredential -// FIXME: can we make these interfaces more uniform or is it okay -// to have quite some differences between them? I think the nice -// thing here is that they are based on the interface from the -// w3c and sd-jwt services. However in that case you could also -// ask why not just require the signed credential as output -// as you can then just call the services yourself. -// FIMXE: add type for type of credential. Also to input of mapper. W3c can be returned for jwt + ldp. and sd-jwt for vc+sd-jwt export type OpenId4VciSignCredential = OpenId4VciSignSdJwtCredential | OpenId4VciSignW3cCredential -export type OpenId4VciSignSdJwtCredential = SdJwtVcSignOptions +export interface OpenId4VciSignSdJwtCredential extends SdJwtVcSignOptions { + format: ClaimFormat.SdJwtVc | `${ClaimFormat.SdJwtVc}` +} + export interface OpenId4VciSignW3cCredential { + format: ClaimFormat.JwtVc | `${ClaimFormat.JwtVc}` | ClaimFormat.LdpVc | `${ClaimFormat.LdpVc}` verificationMethod: string credential: W3cCredential } diff --git a/packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts b/packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts index ea54ad6e4b..6afdcaa40e 100644 --- a/packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts @@ -32,16 +32,9 @@ export interface AccessTokenEndpointConfig { */ endpointPath: string - // FIXME: rename, more specific - /** - * The minimum amount of time in seconds that the client SHOULD wait between polling requests to the Token Endpoint in the Pre-Authorized Code Flow. - * If no value is provided, clients MUST use 5 as the default. - */ - interval?: number - /** * The maximum amount of time in seconds that the pre-authorized code is valid. - * @default 360 (5 minutes) // FIXME: what should be the default value + * @default 360 (5 minutes) */ preAuthorizedCodeExpirationInSeconds: number @@ -49,23 +42,22 @@ export interface AccessTokenEndpointConfig { * The time after which the cNonce from the access token response will * expire. * - * @default 360 (5 minutes) // FIXME: what should be the default value? + * @default 360 (5 minutes) */ cNonceExpiresInSeconds: number /** * The time after which the token will expire. * - * @default 360 (5 minutes) // FIXME: what should be the default value? + * @default 360 (5 minutes) */ tokenExpiresInSeconds: number } export function configureAccessTokenEndpoint(router: Router, config: AccessTokenEndpointConfig) { - const { preAuthorizedCodeExpirationInSeconds } = config router.post( config.endpointPath, - verifyTokenRequest({ preAuthorizedCodeExpirationInSeconds }), + verifyTokenRequest({ preAuthorizedCodeExpirationInSeconds: config.preAuthorizedCodeExpirationInSeconds }), handleTokenRequest(config) ) } @@ -98,9 +90,9 @@ function getJwtSignerCallback(agentContext: AgentContext, signerPublicKey: Key): } export function handleTokenRequest(config: AccessTokenEndpointConfig) { - const { tokenExpiresInSeconds, cNonceExpiresInSeconds, interval } = config + const { tokenExpiresInSeconds, cNonceExpiresInSeconds } = config - return async (request: OpenId4VcIssuanceRequest, response: Response) => { + return async (request: OpenId4VcIssuanceRequest, response: Response, next: NextFunction) => { response.set({ 'Cache-Control': 'no-store', Pragma: 'no-cache' }) const requestContext = getRequestContext(request) @@ -127,25 +119,26 @@ export function handleTokenRequest(config: AccessTokenEndpointConfig) { cNonceExpiresIn: cNonceExpiresInSeconds, cNonces: openId4VcIssuerConfig.getCNonceStateManager(agentContext), accessTokenSignerCallback: getJwtSignerCallback(agentContext, accessTokenSigningKey), - interval, }) - return response.status(200).json(accessTokenResponse) + response.status(200).json(accessTokenResponse) } catch (error) { sendErrorResponse(response, agentContext.config.logger, 400, TokenErrorResponse.invalid_request, error) } + + // NOTE: if we don't call next, the agentContext session handler will NOT be called + next() } } export function verifyTokenRequest(options: { preAuthorizedCodeExpirationInSeconds: number }) { - const { preAuthorizedCodeExpirationInSeconds } = options return async (request: OpenId4VcIssuanceRequest, response: Response, next: NextFunction) => { const { agentContext } = getRequestContext(request) - const openId4VcIssuerConfig = agentContext.dependencyManager.resolve(OpenId4VcIssuerModuleConfig) try { + const openId4VcIssuerConfig = agentContext.dependencyManager.resolve(OpenId4VcIssuerModuleConfig) await assertValidAccessTokenRequest(request.body, { // we use seconds instead of milliseconds for consistency - expirationDuration: preAuthorizedCodeExpirationInSeconds * 1000, + expirationDuration: options.preAuthorizedCodeExpirationInSeconds * 1000, credentialOfferSessions: openId4VcIssuerConfig.getCredentialOfferSessionStateManager(agentContext), }) } catch (error) { @@ -162,6 +155,7 @@ export function verifyTokenRequest(options: { preAuthorizedCodeExpirationInSecon } } - return next() + // NOTE: if we don't call next, the agentContext session handler will NOT be called + next() } } diff --git a/packages/openid4vc/src/openid4vc-issuer/router/credentialEndpoint.ts b/packages/openid4vc/src/openid4vc-issuer/router/credentialEndpoint.ts index c747569015..23f79ba25a 100644 --- a/packages/openid4vc/src/openid4vc-issuer/router/credentialEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-issuer/router/credentialEndpoint.ts @@ -22,20 +22,23 @@ export interface CredentialEndpointConfig { } export function configureCredentialEndpoint(router: Router, config: CredentialEndpointConfig) { - router.post(config.endpointPath, async (request: OpenId4VcIssuanceRequest, response: Response) => { + router.post(config.endpointPath, async (request: OpenId4VcIssuanceRequest, response: Response, next) => { const { agentContext, issuer } = getRequestContext(request) - const openId4VcIssuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService) try { + const openId4VcIssuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService) const credentialRequest = request.body as OpenId4VciCredentialRequest const issueCredentialResponse = await openId4VcIssuerService.createCredentialResponse(agentContext, { issuer, credentialRequest, }) - return response.send(issueCredentialResponse) + response.json(issueCredentialResponse) } catch (error) { sendErrorResponse(response, agentContext.config.logger, 500, 'invalid_request', error) } + + // NOTE: if we don't call next, the agentContext session handler will NOT be called + next() }) } diff --git a/packages/openid4vc/src/openid4vc-issuer/router/metadataEndpoint.ts b/packages/openid4vc/src/openid4vc-issuer/router/metadataEndpoint.ts index db395fe0b8..b3ecb4edc4 100644 --- a/packages/openid4vc/src/openid4vc-issuer/router/metadataEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-issuer/router/metadataEndpoint.ts @@ -6,24 +6,30 @@ import { getRequestContext, sendErrorResponse } from '../../shared/router' import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' export function configureIssuerMetadataEndpoint(router: Router) { - router.get('/.well-known/openid-credential-issuer', (_request: OpenId4VcIssuanceRequest, response: Response) => { - const { agentContext, issuer } = getRequestContext(_request) + router.get( + '/.well-known/openid-credential-issuer', + (_request: OpenId4VcIssuanceRequest, response: Response, next) => { + const { agentContext, issuer } = getRequestContext(_request) - const openId4VcIssuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService) - try { - const issuerMetadata = openId4VcIssuerService.getIssuerMetadata(agentContext, issuer) - const transformedMetadata = { - credential_issuer: issuerMetadata.issuerUrl, - token_endpoint: issuerMetadata.tokenEndpoint, - credential_endpoint: issuerMetadata.credentialEndpoint, - authorization_server: issuerMetadata.authorizationServer, - credentials_supported: issuerMetadata.credentialsSupported, - display: issuerMetadata.issuerDisplay, - } satisfies CredentialIssuerMetadata + try { + const openId4VcIssuerService = agentContext.dependencyManager.resolve(OpenId4VcIssuerService) + const issuerMetadata = openId4VcIssuerService.getIssuerMetadata(agentContext, issuer) + const transformedMetadata = { + credential_issuer: issuerMetadata.issuerUrl, + token_endpoint: issuerMetadata.tokenEndpoint, + credential_endpoint: issuerMetadata.credentialEndpoint, + authorization_server: issuerMetadata.authorizationServer, + credentials_supported: issuerMetadata.credentialsSupported, + display: issuerMetadata.issuerDisplay, + } satisfies CredentialIssuerMetadata - response.status(200).json(transformedMetadata) - } catch (e) { - sendErrorResponse(response, agentContext.config.logger, 500, 'invalid_request', e) + response.status(200).json(transformedMetadata) + } catch (e) { + sendErrorResponse(response, agentContext.config.logger, 500, 'invalid_request', e) + } + + // NOTE: if we don't call next, the agentContext session handler will NOT be called + next() } - }) + ) } diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts index 495141d6c7..def9139dac 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts @@ -112,8 +112,15 @@ export class OpenId4VcVerifierModule implements Module { // Configure endpoints configureAuthorizationEndpoint(endpointRouter, this.config.authorizationEndpoint) - // FIXME: Will this be called when an error occurs / 404 is returned earlier on? - contextRouter.use(async (req: OpenId4VcVerificationRequest, _res, next) => { + // First one will be called for all requests (when next is called) + contextRouter.use(async (req: OpenId4VcVerificationRequest, _res: unknown, next) => { + const { agentContext } = getRequestContext(req) + await agentContext.endSession() + next() + }) + + // This one will be called for all errors that are thrown + contextRouter.use(async (_error: unknown, req: OpenId4VcVerificationRequest, _res: unknown, next: any) => { const { agentContext } = getRequestContext(req) await agentContext.endSession() next() diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierService.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierService.ts index 11139b0306..974f072d9b 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierService.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierService.ts @@ -1,21 +1,16 @@ import type { - ProofRequestWithMetadata, - CreateProofRequestOptions, - ProofRequestMetadata, - VerifiedProofResponse, - VerifierEndpointConfig, + OpenId4VcAuthorizationRequestWithMetadata, + OpenId4VcCreateAuthorizationRequestOptions, + OpenId4VcAuthorizationRequestMetadata, + VerifiedOpenId4VcAuthorizationResponse, HolderMetadata, + OpenId4VcVerifyAuthorizationResponseOptions, } from './OpenId4VcVerifierServiceOptions' -import type { VerificationRequest } from './router/OpenId4VpEndpointConfiguration' import type { AgentContext, W3cVerifyPresentationResult } from '@aries-framework/core' -import type { - AuthorizationResponsePayload, - PresentationVerificationCallback, - SigningAlgo, -} from '@sphereon/did-auth-siop' -import type { NextFunction, Response, Router } from 'express' +import type { PresentationVerificationCallback, SigningAlgo } from '@sphereon/did-auth-siop' import { + utils, joinUriParts, InjectionSymbols, Logger, @@ -25,7 +20,6 @@ import { AriesFrameworkError, W3cJsonLdVerifiablePresentation, JsonTransformer, - AgentContextProvider, } from '@aries-framework/core' import { RP, @@ -41,11 +35,10 @@ import { VerificationMode, AuthorizationResponse, } from '@sphereon/did-auth-siop' -import bodyParser from 'body-parser' -import { getRequestContext } from '../shared/router' +import { storeActorIdForContextCorrelationId } from '../shared/router' +import { getVerifiablePresentationFromSphereonWrapped } from '../shared/transform' import { - generateRandomValues, getSupportedDidMethods, getSuppliedSignatureFromVerificationMethod, getResolver, @@ -53,77 +46,178 @@ import { } from '../shared/utils' import { OpenId4VcVerifierModuleConfig } from './OpenId4VcVerifierModuleConfig' -import { staticOpOpenIdConfig, staticOpSiopConfig } from './OpenId4VcVerifierServiceOptions' -import { configureVerificationEndpoint } from './router/OpenId4VpEndpointConfiguration' +import { OpenId4VcVerifierRecord, OpenId4VcVerifierRepository } from './repository' +import { openidStaticOpConfiguration, siopv2StaticOpConfiguration } from './staticOpConfiguration' /** * @internal */ @injectable() export class OpenId4VcVerifierService { - private logger: Logger - private w3cCredentialService: W3cCredentialService - private openId4VcVerifierModuleConfig: OpenId4VcVerifierModuleConfig - private agentContextProvider: AgentContextProvider + public constructor( + @inject(InjectionSymbols.Logger) private logger: Logger, + private w3cCredentialService: W3cCredentialService, + private openId4VcVerifierRepository: OpenId4VcVerifierRepository, + private config: OpenId4VcVerifierModuleConfig + ) {} - public get verifierMetadata() { - return this.openId4VcVerifierModuleConfig.verifierMetadata - } + public async createAuthorizationRequest( + agentContext: AgentContext, + options: OpenId4VcCreateAuthorizationRequestOptions & { verifier: OpenId4VcVerifierRecord } + ): Promise { + const nonce = await agentContext.wallet.generateNonce() + const state = await agentContext.wallet.generateNonce() + const correlationId = utils.uuid() - public constructor( - @inject(InjectionSymbols.Logger) logger: Logger, - @inject(InjectionSymbols.AgentContextProvider) agentContextProvider: AgentContextProvider, - w3cCredentialService: W3cCredentialService, - openId4VcVerifierModuleConfig: OpenId4VcVerifierModuleConfig - ) { - this.agentContextProvider = agentContextProvider - this.w3cCredentialService = w3cCredentialService - this.logger = logger - this.openId4VcVerifierModuleConfig = openId4VcVerifierModuleConfig + const relyingParty = await this.getRelyingParty(agentContext, options.verifier, options) + const authorizationRequest = await relyingParty.createAuthorizationRequest({ + correlationId, + nonce, + state, + }) + + const authorizationRequestUri = await authorizationRequest.uri() + const encodedAuthorizationRequestUri = authorizationRequestUri.encodedUri + + const metadata = { + nonce, + correlationId, + state, + } + + // FIXME: we need to store some state here? + // Why is the sphereon session manage not enough? + await this.config.getSessionManager(agentContext).saveVerifyProofResponseOptions(correlationId, { + createProofRequestOptions: options, + proofRequestMetadata: metadata, + }) + + return { + authorizationRequestUri: encodedAuthorizationRequestUri, + metadata, + } } - public async getRelyingParty( + public async verifyAuthorizationResponse( agentContext: AgentContext, - createProofRequestOptions: CreateProofRequestOptions, - proofRequestMetadata?: ProofRequestMetadata - ) { - const { - verificationEndpointUrl, - presentationDefinition, - verificationMethod, - holderMetadata: _holderClientMetadata, - } = createProofRequestOptions + options: OpenId4VcVerifyAuthorizationResponseOptions & { verifier: OpenId4VcVerifierRecord } + ): Promise { + const authorizationResponse = await AuthorizationResponse.fromPayload(options.authorizationResponse).catch(() => { + throw new AriesFrameworkError( + `Unable to parse authorization response payload. ${JSON.stringify(options.authorizationResponse)}` + ) + }) - const isVpRequest = presentationDefinition !== undefined + // FIXME: we need to rework the custom verification state stuff + const resNonce = await authorizationResponse.getMergedProperty('nonce', false) + const resState = await authorizationResponse.getMergedProperty('state', false) + const sessionManager = this.config.getSessionManager(agentContext) - let holderClientMetadata: HolderMetadata - if (!_holderClientMetadata) { - // use a static set of configuration values defined in the spec - if (isVpRequest) { - holderClientMetadata = staticOpOpenIdConfig - } else { - holderClientMetadata = staticOpSiopConfig - } - } else { - if (typeof _holderClientMetadata === 'string') { - // Use OpenId Discovery to get the client metadata - let reference_uri = _holderClientMetadata - if (!reference_uri.endsWith('/.well-known/openid-configuration')) { - reference_uri = reference_uri + '/.well-known/openid-configuration' + const correlationId = resNonce + ? await sessionManager.getCorrelationIdByNonce(resNonce, false) + : resState + ? await sessionManager.getCorrelationIdByState(resState, false) + : undefined + + if (!correlationId) { + throw new AriesFrameworkError(`Unable to find correlationId for nonce '${resNonce}' or state '${resState}'`) + } + + const verifyProofResponseOptions = await sessionManager.getVerifyProofResponseOptions(correlationId) + if (!verifyProofResponseOptions) { + throw new AriesFrameworkError(`Unable to associate a request to the response correlationId '${correlationId}'`) + } + + const { createProofRequestOptions, proofRequestMetadata } = verifyProofResponseOptions + const presentationDefinition = createProofRequestOptions.presentationDefinition + + // For now we always use the VP_TOKEN + const presentationDefinitionsWithLocation = presentationDefinition + ? [{ definition: presentationDefinition, location: PresentationDefinitionLocation.CLAIMS_VP_TOKEN }] + : undefined + + const relyingParty = await this.getRelyingParty( + agentContext, + options.verifier, + createProofRequestOptions, + proofRequestMetadata + ) + + const response = await relyingParty.verifyAuthorizationResponse(authorizationResponse.payload, { + // FIXME: can be extracted from iss of request? + audience: createProofRequestOptions.verificationMethod.id, + correlationId, + // FIXME: can be extracted from request? + nonce: proofRequestMetadata.nonce, + // FIXME: can be extracted from request? + state: proofRequestMetadata.state, + presentationDefinitions: presentationDefinitionsWithLocation, + // FIXME: does this verify the VP as well? Or just the id_token and the vp_token submission, + // but not the actual signature on the VP? + // -> I think it uses the presentation verification callback + verification: { + mode: VerificationMode.INTERNAL, + resolveOpts: { noUniversalResolverFallback: true, resolver: getResolver(agentContext) }, + }, + }) + + const presentationExchange = response.oid4vpSubmission + ? { + submission: response.oid4vpSubmission?.submissionData, + definitions: response.oid4vpSubmission?.presentationDefinitions.map((d) => d.definition), + presentations: response.oid4vpSubmission?.presentations.map(getVerifiablePresentationFromSphereonWrapped), } - holderClientMetadata = { reference_uri, passBy: PassBy.REFERENCE, targets: PropertyTarget.REQUEST_OBJECT } - } else { - holderClientMetadata = _holderClientMetadata - } + : undefined + + return { + // FIXME: rename and only extract needed payload, don't want sphereon types to be exposed on AFJ layer + idTokenPayload: await response.authorizationResponse.idToken.payload(), + + // Parameters related to DIF Presentation Exchange + presentationExchange, } + } + + public async getAllVerifiers(agentContext: AgentContext) { + return this.openId4VcVerifierRepository.getAll(agentContext) + } + + public async getByVerifierId(agentContext: AgentContext, verifierId: string) { + return this.openId4VcVerifierRepository.getByVerifierId(agentContext, verifierId) + } + + public async updateVerifier(agentContext: AgentContext, verifier: OpenId4VcVerifierRecord) { + return this.openId4VcVerifierRepository.update(agentContext, verifier) + } + + public async createVerifier(agentContext: AgentContext) { + const openId4VcVerifier = new OpenId4VcVerifierRecord({ + verifierId: utils.uuid(), + }) + + await this.openId4VcVerifierRepository.save(agentContext, openId4VcVerifier) + await storeActorIdForContextCorrelationId(agentContext, openId4VcVerifier.verifierId) + return openId4VcVerifier + } + + private async getRelyingParty( + agentContext: AgentContext, + verifier: OpenId4VcVerifierRecord, + createAuthorizationRequestOptions: OpenId4VcCreateAuthorizationRequestOptions, + proofRequestMetadata?: OpenId4VcAuthorizationRequestMetadata + ) { + const { verificationEndpointUrl, presentationDefinition, verificationMethod } = createAuthorizationRequestOptions + + const isVpRequest = presentationDefinition !== undefined + const openIdConfiguration = this.getOpenIdConfiguration(createAuthorizationRequestOptions) const { signature, did, kid, alg } = await getSuppliedSignatureFromVerificationMethod( agentContext, verificationMethod ) - // Check if the OpenId Provider (Holder) can validate the request signature provided by the Relying Party (Verifier) - const requestObjectSigningAlgValuesSupported = holderClientMetadata.requestObjectSigningAlgValuesSupported + // Check if the OpenId Provider can validate the request signature provided by the Relying Party + const requestObjectSigningAlgValuesSupported = openIdConfiguration.requestObjectSigningAlgValuesSupported if (requestObjectSigningAlgValuesSupported && !requestObjectSigningAlgValuesSupported.includes(alg)) { throw new AriesFrameworkError( [ @@ -134,17 +228,19 @@ export class OpenId4VcVerifierService { } // Check if the Relying Party (Verifier) can validate the IdToken provided by the OpenId Provider (Holder) - const idTokenSigningAlgValuesSupported = holderClientMetadata.idTokenSigningAlgValuesSupported + // FIXME: This might cause issues as the static configuration is very limited and thus it would + // prevent any new algorithms from being used. + const idTokenSigningAlgValuesSupported = openIdConfiguration.idTokenSigningAlgValuesSupported if (idTokenSigningAlgValuesSupported) { const rpSupportedSignatureAlgorithms = getSupportedJwaSignatureAlgorithms( agentContext ) as unknown as SigningAlgo[] - const possibleIdTokenSigningAlgValues = Array.isArray(idTokenSigningAlgValuesSupported) - ? idTokenSigningAlgValuesSupported.filter((value) => rpSupportedSignatureAlgorithms.includes(value)) + const possibleIdTokenSigningAlgValue = Array.isArray(idTokenSigningAlgValuesSupported) + ? idTokenSigningAlgValuesSupported.some((value) => rpSupportedSignatureAlgorithms.includes(value)) : rpSupportedSignatureAlgorithms.includes(idTokenSigningAlgValuesSupported) - if (!possibleIdTokenSigningAlgValues) { + if (!possibleIdTokenSigningAlgValue) { throw new AriesFrameworkError( [ `The OpenId Provider supports no signature algorithms that are supported by the Relying Party.`, @@ -155,14 +251,12 @@ export class OpenId4VcVerifierService { } } - const authorizationEndpoint = holderClientMetadata?.authorization_endpoint ?? (isVpRequest ? 'openid:' : 'siopv2:') - + // FIXME: what do we call this? RP url? There should be an openid name for it + const relyingPartyUrl = joinUriParts(this.config.baseUrl, [verifier.verifierId]) + // FIXME: is it authorization endpoint? What do you call the endpoint where you + // submit the authorization response to? const redirectUri = - verificationEndpointUrl ?? - joinUriParts(this.verifierMetadata.verifierBaseUrl, [ - this.openId4VcVerifierModuleConfig.getBasePath(agentContext), - this.verifierMetadata.verificationEndpointPath, - ]) + verificationEndpointUrl ?? joinUriParts(relyingPartyUrl, [this.config.authorizationEndpoint.endpointPath]) // Check: audience must be set to the issuer with dynamic disc otherwise self-issued.me/v2. const builder = RP.builder() @@ -171,22 +265,32 @@ export class OpenId4VcVerifierService { .withIssuer(ResponseIss.SELF_ISSUED_V2) .withSuppliedSignature(signature, did, kid, alg) .withSupportedVersions([SupportedVersion.SIOPv2_D11, SupportedVersion.SIOPv2_D12_OID4VP_D18]) - .withClientMetadata(holderClientMetadata) + // FIXME: client metadata is the metadata of the RP + // but it's being added as OP metadata + // Is this both OP and Client metadata? + .withClientMetadata(openIdConfiguration) .withCustomResolver(getResolver(agentContext)) .withResponseMode(ResponseMode.POST) .withResponseType(isVpRequest ? [ResponseType.ID_TOKEN, ResponseType.VP_TOKEN] : ResponseType.ID_TOKEN) .withScope('openid') .withRequestBy(PassBy.VALUE) - .withAuthorizationEndpoint(authorizationEndpoint) .withCheckLinkedDomain(CheckLinkedDomain.NEVER) + // FIXME: should allow verification of revocation + // .withRevocationVerificationCallback() .withRevocationVerification(RevocationVerification.NEVER) - .withSessionManager(this.openId4VcVerifierModuleConfig.getSessionManager(agentContext)) - .withEventEmitter(this.openId4VcVerifierModuleConfig.getEventEmitter(agentContext)) - // .withWellknownDIDVerifyCallback + .withSessionManager(this.config.getSessionManager(agentContext)) + .withEventEmitter(this.config.getEventEmitter(agentContext)) + if (openIdConfiguration.authorization_endpoint) { + builder.withAuthorizationEndpoint(openIdConfiguration.authorization_endpoint) + } + + // FIXME: we don't want to pass proof request metadata + // Maybe we can just return the builder, and add it in the caller method? + // Or we somehow dynamically get the nonce from the request? if (proofRequestMetadata) { builder.withPresentationVerification( - this.getPresentationVerificationCallback(agentContext, { challenge: proofRequestMetadata.challenge }) + this.getPresentationVerificationCallback(agentContext, { challenge: proofRequestMetadata.nonce }) ) } @@ -205,98 +309,7 @@ export class OpenId4VcVerifierService { return builder.build() } - public async createProofRequest( - agentContext: AgentContext, - options: CreateProofRequestOptions - ): Promise { - const [noncePart1, noncePart2, state, correlationId] = await generateRandomValues(agentContext, 4) - const challenge = noncePart1 + noncePart2 - - const relyingParty = await this.getRelyingParty(agentContext, options) - - const authorizationRequest = await relyingParty.createAuthorizationRequest({ - correlationId, - nonce: challenge, - state, - }) - - const authorizationRequestUri = await authorizationRequest.uri() - const encodedAuthorizationRequestUri = authorizationRequestUri.encodedUri - - const proofRequestMetadata = { correlationId, challenge, state } - - await this.openId4VcVerifierModuleConfig - .getSessionManager(agentContext) - .saveVerifyProofResponseOptions(correlationId, { - createProofRequestOptions: options, - proofRequestMetadata, - }) - - return { - proofRequest: encodedAuthorizationRequestUri, - proofRequestMetadata, - } - } - - public async verifyProofResponse( - agentContext: AgentContext, - authorizationResponsePayload: AuthorizationResponsePayload - ): Promise { - let authorizationResponse: AuthorizationResponse - try { - authorizationResponse = await AuthorizationResponse.fromPayload(authorizationResponsePayload) - } catch (error: unknown) { - throw new AriesFrameworkError( - `Unable to parse authorization response payload. ${JSON.stringify(authorizationResponsePayload)}` - ) - } - - const resNonce = (await authorizationResponse.getMergedProperty('nonce', false)) as string - const resState = (await authorizationResponse.getMergedProperty('state', false)) as string - const sessionManager = this.openId4VcVerifierModuleConfig.getSessionManager(agentContext) - - const correlationId = - (await sessionManager.getCorrelationIdByNonce(resNonce, false)) ?? - (await sessionManager.getCorrelationIdByState(resState, false)) - if (!correlationId) { - throw new AriesFrameworkError(`Unable to find correlationId for nonce '${resNonce}' or state '${resState}'`) - } - - const verifyProofResponseOptions = await sessionManager.getVerifyProofResponseOptions(correlationId) - if (!verifyProofResponseOptions) { - throw new AriesFrameworkError(`Unable to associate a request to the response correlationId '${correlationId}'`) - } - - const { createProofRequestOptions, proofRequestMetadata } = verifyProofResponseOptions - const presentationDefinition = createProofRequestOptions.presentationDefinition - - // For now we always use the VP_TOKEN - const presentationDefinitionsWithLocation = presentationDefinition - ? [{ definition: presentationDefinition, location: PresentationDefinitionLocation.CLAIMS_VP_TOKEN }] - : undefined - - const relyingParty = await this.getRelyingParty(agentContext, createProofRequestOptions, proofRequestMetadata) - - const response = await relyingParty.verifyAuthorizationResponse(authorizationResponsePayload, { - audience: createProofRequestOptions.verificationMethod.id, - correlationId, - nonce: proofRequestMetadata.challenge, - state: proofRequestMetadata.state, - presentationDefinitions: presentationDefinitionsWithLocation, - verification: { - mode: VerificationMode.INTERNAL, - resolveOpts: { noUniversalResolverFallback: true, resolver: getResolver(agentContext) }, - }, - }) - - const idTokenPayload = await response.authorizationResponse.idToken.payload() - - return { - idTokenPayload: idTokenPayload, - submission: presentationDefinition ? response.oid4vpSubmission : undefined, - } - } - + // FIXME: does the higher level check whether the iss of the VP is ok? private getPresentationVerificationCallback( agentContext: AgentContext, options: { challenge: string } @@ -325,51 +338,43 @@ export class OpenId4VcVerifierService { } } - public configureRouter = ( - initializationContext: AgentContext, - router: Router, - endpointConfig: VerifierEndpointConfig - ) => { - const { basePath } = endpointConfig - this.openId4VcVerifierModuleConfig.setBasePath(initializationContext, basePath) + private getOpenIdConfiguration(options: OpenId4VcCreateAuthorizationRequestOptions): HolderMetadata { + const isVpRequest = options.presentationDefinition !== undefined - // parse application/x-www-form-urlencoded - router.use(bodyParser.urlencoded({ extended: false })) - - // parse application/json - router.use(bodyParser.json()) - - // initialize the agent and set the request context - router.use(async (req: VerificationRequest, _res: Response, next: NextFunction) => { - const agentContext = await this.agentContextProvider.getAgentContextForContextCorrelationId( - initializationContext.contextCorrelationId - ) + // Not provided, use default static configurations + if (!options.openIdProvider) { + return isVpRequest ? openidStaticOpConfiguration : siopv2StaticOpConfiguration + } - req.requestContext = { - agentContext, - openId4VcVerifierService: agentContext.dependencyManager.resolve(OpenId4VcVerifierService), - logger: agentContext.dependencyManager.resolve(InjectionSymbols.Logger), + // siopv2: provided or not provided and not vp request + if (options.openIdProvider === 'siopv2:') { + if (isVpRequest) { + throw new AriesFrameworkError( + "Cannot use 'siopv2:' as OP configuration when a presentation definition is provided. Use 'openid:' instead." + ) } + return siopv2StaticOpConfiguration + } - next() - }) - - if (endpointConfig.verificationEndpointConfig?.enabled) { - const verificationEndpointPath = this.verifierMetadata.verificationEndpointPath - configureVerificationEndpoint(router, verificationEndpointPath, { - ...endpointConfig.verificationEndpointConfig, - }) - - const endPointUrl = joinUriParts(this.verifierMetadata.verifierBaseUrl, [basePath, verificationEndpointPath]) - this.logger.info(`[OID4VP] Verification endpoint running at '${endPointUrl}'.`) + // openid: provided or not provided and vp request + if (options.openIdProvider === 'openid:') { + return openidStaticOpConfiguration } - router.use(async (req: VerificationRequest, _res, next) => { - const { agentContext } = getRequestContext(req) - await agentContext.endSession() - next() - }) + // if string it MUST be an url + if (typeof options.openIdProvider === 'string') { + // TODO: add url validation + const referenceUri = options.openIdProvider.includes('/.well-known/openid-configuration') + ? options.openIdProvider + : joinUriParts(options.openIdProvider, ['/.well-known/openid-configuration']) + + return { + reference_uri: referenceUri, + passBy: PassBy.REFERENCE, + targets: PropertyTarget.REQUEST_OBJECT, + } + } - return router + return options.openIdProvider } } diff --git a/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.e2e.test.ts b/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.e2e.test.ts index d6ce60356e..b80d752396 100644 --- a/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.e2e.test.ts @@ -44,7 +44,7 @@ describe('OpenId4VcVerifier', () => { it(`cannot sign authorization request with alg that isn't supported by the OpenId Provider`, async () => { await expect( - verifier.agent.modules.openId4VcVerifier.createProofRequest({ + verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ verificationEndpointUrl: 'http://redirect-uri', verificationMethod: verifier.verificationMethod, }) @@ -52,21 +52,21 @@ describe('OpenId4VcVerifier', () => { }) it(`check openid proof request format`, async () => { - const { proofRequest } = await verifier.agent.modules.openId4VcVerifier.createProofRequest({ + const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ verificationEndpointUrl: 'http://redirect-uri', verificationMethod: verifier.verificationMethod, - holderMetadata: staticOpOpenIdConfigEdDSA, + openIdProvider: staticOpOpenIdConfigEdDSA, presentationDefinition: universityDegreePresentationDefinition, }) const base = 'openid://?redirect_uri=http%3A%2F%2Fredirect-uri&presentation_definition=%7B%22id%22%3A%22UniversityDegreeCredential%22%2C%22input_descriptors%22%3A%5B%7B%22id%22%3A%22UniversityDegree%22%2C%22format%22%3A%7B%22jwt_vc%22%3A%7B%22alg%22%3A%5B%22EdDSA%22%5D%7D%7D%2C%22constraints%22%3A%7B%22fields%22%3A%5B%7B%22path%22%3A%5B%22%24.vc.type.*%22%5D%2C%22filter%22%3A%7B%22type%22%3A%22string%22%2C%22pattern%22%3A%22UniversityDegree%22%7D%7D%5D%7D%7D%5D%7D&request=' - expect(proofRequest.startsWith(base)).toBe(true) + expect(authorizationRequestUri.startsWith(base)).toBe(true) - const _jwt = proofRequest.substring(base.length) + const _jwt = authorizationRequestUri.substring(base.length) const jwt = Jwt.fromSerializedJwt(_jwt) - expect(proofRequest.startsWith(base)).toBe(true) + expect(authorizationRequestUri.startsWith(base)).toBe(true) expect(jwt.header.kid).toEqual(verifier.kid) expect(jwt.header.alg).toEqual(SigningAlgo.EDDSA) @@ -83,17 +83,17 @@ describe('OpenId4VcVerifier', () => { }) it(`check siop proof request format`, async () => { - const { proofRequest } = await verifier.agent.modules.openId4VcVerifier.createProofRequest({ + const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ verificationEndpointUrl: 'http://redirect-uri', verificationMethod: verifier.verificationMethod, - holderMetadata: staticSiopConfigEDDSA, + openIdProvider: staticSiopConfigEDDSA, }) // TODO: this should be siopv2 const base = 'openid://?redirect_uri=http%3A%2F%2Fredirect-uri&request=' - expect(proofRequest.startsWith(base)).toBe(true) + expect(authorizationRequestUri.startsWith(base)).toBe(true) - const _jwt = proofRequest.substring(base.length) + const _jwt = authorizationRequestUri.substring(base.length) const jwt = Jwt.fromSerializedJwt(_jwt) expect(jwt.header.kid).toEqual(verifier.kid) diff --git a/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcVerifierRecord.ts b/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcVerifierRecord.ts index e493faa2d6..f6fca64581 100644 --- a/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcVerifierRecord.ts +++ b/packages/openid4vc/src/openid4vc-verifier/repository/OpenId4VcVerifierRecord.ts @@ -16,7 +16,6 @@ export interface OpenId4VcVerifierRecordProps { verifierId: string } -// FIXME: combine with issuer record? export class OpenId4VcVerifierRecord extends BaseRecord { public static readonly type = 'OpenId4VcVerifierRecord' public readonly type = OpenId4VcVerifierRecord.type diff --git a/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts b/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts index 5ca5b64f7c..2b73927f92 100644 --- a/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts @@ -16,7 +16,7 @@ export interface AuthorizationEndpointConfig { } export function configureAuthorizationEndpoint(router: Router, config: AuthorizationEndpointConfig) { - router.post(config.endpointPath, async (request: OpenId4VcVerificationRequest, response: Response) => { + router.post(config.endpointPath, async (request: OpenId4VcVerificationRequest, response: Response, next) => { const { agentContext, verifier } = getRequestContext(request) try { @@ -31,9 +31,12 @@ export function configureAuthorizationEndpoint(router: Router, config: Authoriza authorizationResponse: request.body, verifier, }) - return response.status(200).send() + response.status(200).send() } catch (error) { - return sendErrorResponse(response, agentContext.config.logger, 500, 'invalid_request', error) + sendErrorResponse(response, agentContext.config.logger, 500, 'invalid_request', error) } + + // NOTE: if we don't call next, the agentContext session handler will NOT be called + next() }) } diff --git a/packages/openid4vc/src/shared/models/CredentialHolderBinding.ts b/packages/openid4vc/src/shared/models/CredentialHolderBinding.ts index 0796e9b213..641df91c2c 100644 --- a/packages/openid4vc/src/shared/models/CredentialHolderBinding.ts +++ b/packages/openid4vc/src/shared/models/CredentialHolderBinding.ts @@ -1,13 +1,13 @@ import type { Jwk } from '@aries-framework/core' -export type CredentialHolderDidBinding = { +export type OpenId4VcCredentialHolderDidBinding = { method: 'did' didUrl: string } -export type CredentialHolderJwkBinding = { +export type OpenId4VcCredentialHolderJwkBinding = { method: 'jwk' jwk: Jwk } -export type CredentialHolderBinding = CredentialHolderDidBinding | CredentialHolderJwkBinding +export type OpenId4VcCredentialHolderBinding = OpenId4VcCredentialHolderDidBinding | OpenId4VcCredentialHolderJwkBinding diff --git a/packages/openid4vc/src/shared/router/tenants.ts b/packages/openid4vc/src/shared/router/tenants.ts index 93f92164f8..23b5353da4 100644 --- a/packages/openid4vc/src/shared/router/tenants.ts +++ b/packages/openid4vc/src/shared/router/tenants.ts @@ -40,7 +40,6 @@ export async function storeActorIdForContextCorrelationId(agentContext: AgentCon // It's kind of hacky, but we add support for the tenants module specifically here to map an actorId to // a specific tenant. Otherwise we have to expose /:contextCorrelationId/:actorId in all the public URLs // which is of course not so nice. - // FIXME: it's maybe nicer to just depend on the tenants module const tenantsApi = getApiForModuleByName(agentContext, 'TenantsModule') // We don't want to query the tenant record if the current context is the root context diff --git a/packages/openid4vc/src/shared/transform.ts b/packages/openid4vc/src/shared/transform.ts index bd7ffa9648..5654879721 100644 --- a/packages/openid4vc/src/shared/transform.ts +++ b/packages/openid4vc/src/shared/transform.ts @@ -1,78 +1,58 @@ import type { W3cVerifiableCredential, W3cVerifiablePresentation } from '@aries-framework/core' +import type { SdJwtVc } from '@aries-framework/sd-jwt-vc' import type { - OriginalVerifiableCredential as SphereonOriginalVerifiableCredential, W3CVerifiableCredential as SphereonW3cVerifiableCredential, - W3CVerifiablePresentation as SphereonW3cVerifiablePresentation, + CompactSdJwtVc as SphereonCompactSdJwtVc, + WrappedVerifiablePresentation, } from '@sphereon/ssi-types' import { - ClaimFormat, JsonTransformer, AriesFrameworkError, W3cJsonLdVerifiablePresentation, W3cJwtVerifiablePresentation, W3cJwtVerifiableCredential, W3cJsonLdVerifiableCredential, + JsonEncoder, } from '@aries-framework/core' -export function getSphereonOriginalVerifiableCredential( - w3cVerifiableCredential: W3cVerifiableCredential -): SphereonOriginalVerifiableCredential { - if (w3cVerifiableCredential.claimFormat === ClaimFormat.LdpVc) { - return JsonTransformer.toJSON(w3cVerifiableCredential) as SphereonOriginalVerifiableCredential - } else if (w3cVerifiableCredential.claimFormat === ClaimFormat.JwtVc) { - return w3cVerifiableCredential.serializedJwt +export function getSphereonVerifiableCredential( + verifiableCredential: W3cVerifiableCredential | SdJwtVc +): SphereonW3cVerifiableCredential | SphereonCompactSdJwtVc { + // encoded sd-jwt or jwt + if (typeof verifiableCredential === 'string') { + return verifiableCredential + } else if (verifiableCredential instanceof W3cJsonLdVerifiableCredential) { + return JsonTransformer.toJSON(verifiableCredential) as SphereonW3cVerifiableCredential + } else if (verifiableCredential instanceof W3cJwtVerifiableCredential) { + return verifiableCredential.serializedJwt } else { - throw new AriesFrameworkError( - `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` - ) + return verifiableCredential.compact } } -export function getSphereonW3cVerifiableCredential( - w3cVerifiableCredential: W3cVerifiableCredential -): SphereonW3cVerifiableCredential { - if (w3cVerifiableCredential.claimFormat === ClaimFormat.LdpVc) { - return JsonTransformer.toJSON(w3cVerifiableCredential) as SphereonW3cVerifiableCredential - } else if (w3cVerifiableCredential.claimFormat === ClaimFormat.JwtVc) { - return w3cVerifiableCredential.serializedJwt - } else { - throw new AriesFrameworkError( - `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` - ) - } -} +export function getVerifiablePresentationFromSphereonWrapped( + wrappedVerifiablePresentation: WrappedVerifiablePresentation +): W3cVerifiablePresentation | SdJwtVc { + if (wrappedVerifiablePresentation.format === 'jwt_vp') { + if (typeof wrappedVerifiablePresentation.original !== 'string') { + throw new AriesFrameworkError('Unable to transform JWT VP to W3C VP') + } -export function getSphereonW3cVerifiablePresentation( - w3cVerifiablePresentation: W3cVerifiablePresentation -): SphereonW3cVerifiablePresentation { - if (w3cVerifiablePresentation instanceof W3cJsonLdVerifiablePresentation) { - return JsonTransformer.toJSON(w3cVerifiablePresentation) as SphereonW3cVerifiablePresentation - } else if (w3cVerifiablePresentation instanceof W3cJwtVerifiablePresentation) { - return w3cVerifiablePresentation.serializedJwt - } else { - throw new AriesFrameworkError( - `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` - ) + return W3cJwtVerifiablePresentation.fromSerializedJwt(wrappedVerifiablePresentation.original) + } else if (wrappedVerifiablePresentation.format === 'ldp_vp') { + return JsonTransformer.fromJSON(wrappedVerifiablePresentation.original, W3cJsonLdVerifiablePresentation) + } else if (wrappedVerifiablePresentation.format === 'vc+sd-jwt') { + // We use some custom logic here so we don't have to re-process the encoded SD-JWT + const [encodedHeader] = wrappedVerifiablePresentation.presentation.compactSdJwtVc.split('.') + const header = JsonEncoder.fromBase64(encodedHeader) + return { + compact: wrappedVerifiablePresentation.presentation.compactSdJwtVc, + header, + payload: wrappedVerifiablePresentation.presentation.signedPayload, + prettyClaims: wrappedVerifiablePresentation.presentation.decodedPayload, + } satisfies SdJwtVc } -} -export function getW3cVerifiablePresentationInstance( - w3cVerifiablePresentation: SphereonW3cVerifiablePresentation -): W3cVerifiablePresentation { - if (typeof w3cVerifiablePresentation === 'string') { - return W3cJwtVerifiablePresentation.fromSerializedJwt(w3cVerifiablePresentation) - } else { - return JsonTransformer.fromJSON(w3cVerifiablePresentation, W3cJsonLdVerifiablePresentation) - } -} - -export function getW3cVerifiableCredentialInstance( - w3cVerifiableCredential: SphereonW3cVerifiableCredential -): W3cVerifiableCredential { - if (typeof w3cVerifiableCredential === 'string') { - return W3cJwtVerifiableCredential.fromSerializedJwt(w3cVerifiableCredential) - } else { - return JsonTransformer.fromJSON(w3cVerifiableCredential, W3cJsonLdVerifiableCredential) - } + throw new AriesFrameworkError(`Unsupported presentation format: ${wrappedVerifiablePresentation.format}`) } diff --git a/packages/openid4vc/tests/openid4vc.e2e.test.ts b/packages/openid4vc/tests/openid4vc.e2e.test.ts index 42763a28e0..1fadc089b5 100644 --- a/packages/openid4vc/tests/openid4vc.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc.e2e.test.ts @@ -1,9 +1,8 @@ import type { AgentType, TenantType } from './utils' -import type { CredentialBindingResolver } from '../src/openid4vc-holder' -import type { SdJwtVc, SdJwtVcSignOptions } from '@aries-framework/sd-jwt-vc' +import type { OpenId4VciCredentialBindingResolver } from '../src/openid4vc-holder' +import type { SdJwtVc } from '@aries-framework/sd-jwt-vc' import type { Server } from 'http' -import { AskarModule } from '@aries-framework/askar' import { ClaimFormat, JwaSignatureAlgorithm, @@ -17,12 +16,12 @@ import { AriesFrameworkError, DifPresentationExchangeService, } from '@aries-framework/core' -import { SdJwtVcModule } from '@aries-framework/sd-jwt-vc' -import { TenantsModule } from '@aries-framework/tenants' -import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import express, { type Express } from 'express' +import { AskarModule } from '../../askar/src' import { askarModuleConfig } from '../../askar/tests/helpers' +import { SdJwtVcModule } from '../../sd-jwt-vc/src' +import { TenantsModule } from '../../tenants/src' import { OpenId4VcVerifierModule, OpenId4VcHolderModule, OpenId4VcIssuerModule } from '../src' import { createAgentFromModules, createTenantForAgent } from './utils' @@ -91,6 +90,7 @@ describe('OpenId4Vc', () => { if (credentialRequest.format === 'vc+sd-jwt') { return { + format: credentialRequest.format, payload: { vct: credentialRequest.vct, university: 'innsbruck', degree: 'bachelor' }, holder: holderBinding, issuer: { @@ -98,7 +98,7 @@ describe('OpenId4Vc', () => { didUrl: verificationMethod.id, }, disclosureFrame: { university: true, degree: true }, - } satisfies SdJwtVcSignOptions + } } throw new Error('Invalid request') @@ -134,7 +134,7 @@ describe('OpenId4Vc', () => { baseUrl: verificationBaseUrl, }), sdJwtVc: new SdJwtVcModule(), - askar: new AskarModule({ ariesAskar }), + askar: new AskarModule(askarModuleConfig), tenants: new TenantsModule(), }, '96213c3d7fc8d4d6754c7a0fd969598f' @@ -159,7 +159,7 @@ describe('OpenId4Vc', () => { await holder.agent.wallet.delete() }) - const credentialBindingResolver: CredentialBindingResolver = ({ supportsJwk, supportedDidMethods }) => { + const credentialBindingResolver: OpenId4VciCredentialBindingResolver = ({ supportsJwk, supportedDidMethods }) => { // prefer did:key if (supportedDidMethods?.includes('did:key')) { return { diff --git a/packages/openid4vc/tests/utils.ts b/packages/openid4vc/tests/utils.ts index 725f909ca7..88007013f8 100644 --- a/packages/openid4vc/tests/utils.ts +++ b/packages/openid4vc/tests/utils.ts @@ -3,15 +3,14 @@ import type { KeyDidCreateOptions, ModulesMap } from '@aries-framework/core' import type { TenantsModule } from '@aries-framework/tenants' import { LogLevel, Agent, DidKey, KeyType, TypedArrayEncoder, utils } from '@aries-framework/core' -import { agentDependencies } from '@aries-framework/node' -import { TestLogger } from '../../core/tests/logger' +import { agentDependencies, TestLogger } from '../../core/tests' -export async function createDidKidVerificationMethod(agent: Agent | TenantAgent, secretKey: string) { +export async function createDidKidVerificationMethod(agent: Agent | TenantAgent, secretKey?: string) { const didCreateResult = await agent.dids.create({ method: 'key', options: { keyType: KeyType.Ed25519 }, - secret: { privateKey: TypedArrayEncoder.fromString(secretKey) }, + secret: { privateKey: secretKey ? TypedArrayEncoder.fromString(secretKey) : undefined }, }) const did = didCreateResult.didState.did as string @@ -62,12 +61,8 @@ export async function createTenantForAgent( }, }) - const nonce1 = await agent.wallet.generateNonce() - const nonce2 = await agent.wallet.generateNonce() - const secretKey = (nonce1 + nonce2).slice(0, 32) - const tenant = await agent.modules.tenants.getTenantAgent({ tenantId: tenantRecord.id }) - const data = await createDidKidVerificationMethod(tenant, secretKey) + const data = await createDidKidVerificationMethod(tenant) await tenant.endSession() return { From d7c64f0dde6527bf096cf3aadf2e1cc18cafd519 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Wed, 24 Jan 2024 15:44:41 +0700 Subject: [PATCH 108/115] chore: move sd-jwt-vc to core Signed-off-by: Timo Glastra --- demo-openid/package.json | 1 - demo-openid/src/Holder.ts | 4 +- demo-openid/src/HolderInquirer.ts | 2 +- demo-openid/src/Issuer.ts | 3 - packages/core/package.json | 1 + packages/core/src/agent/AgentModules.ts | 2 + packages/core/src/agent/BaseAgent.ts | 4 + packages/core/src/index.ts | 1 + .../src/modules/sd-jwt-vc}/SdJwtVcApi.ts | 5 +- .../src/modules/sd-jwt-vc/SdJwtVcError.ts | 3 + .../src/modules/sd-jwt-vc}/SdJwtVcModule.ts | 6 +- .../src/modules/sd-jwt-vc}/SdJwtVcOptions.ts | 3 +- .../src/modules/sd-jwt-vc}/SdJwtVcService.ts | 50 +++-- .../__tests__/SdJwtVcModule.test.ts | 0 .../__tests__/SdJwtVcService.test.ts | 32 +-- .../sd-jwt-vc/__tests__}/sdJwtVc.e2e.test.ts | 26 ++- .../sd-jwt-vc}/__tests__/sdjwtvc.fixtures.ts | 0 .../src/modules/sd-jwt-vc}/index.ts | 0 .../sd-jwt-vc}/repository/SdJwtVcRecord.ts | 11 +- .../repository/SdJwtVcRepository.ts | 6 +- .../__tests__/SdJwtVcRecord.test.ts | 4 +- .../modules/sd-jwt-vc}/repository/index.ts | 0 .../OpenId4VciHolderService.ts | 19 +- .../__tests__/openid4vci-holder.e2e.test.ts | 13 +- .../__tests__/openid4vp-holder.e2e.test.ts | 198 +++++++++--------- .../OpenId4VcIssuerService.ts | 6 +- .../__tests__/openid4vc-issuer.e2e.test.ts | 11 +- .../OpenId4VcVerifierService.ts | 4 +- .../openid4vc/tests/openid4vc.e2e.test.ts | 13 +- packages/sd-jwt-vc/README.md | 57 ----- packages/sd-jwt-vc/jest.config.ts | 13 -- packages/sd-jwt-vc/package.json | 40 ---- packages/sd-jwt-vc/src/SdJwtVcError.ts | 3 - packages/sd-jwt-vc/tests/setup.ts | 3 - packages/sd-jwt-vc/tsconfig.build.json | 7 - packages/sd-jwt-vc/tsconfig.json | 6 - 36 files changed, 220 insertions(+), 337 deletions(-) rename packages/{sd-jwt-vc/src => core/src/modules/sd-jwt-vc}/SdJwtVcApi.ts (95%) create mode 100644 packages/core/src/modules/sd-jwt-vc/SdJwtVcError.ts rename packages/{sd-jwt-vc/src => core/src/modules/sd-jwt-vc}/SdJwtVcModule.ts (67%) rename packages/{sd-jwt-vc/src => core/src/modules/sd-jwt-vc}/SdJwtVcOptions.ts (96%) rename packages/{sd-jwt-vc/src => core/src/modules/sd-jwt-vc}/SdJwtVcService.ts (91%) rename packages/{sd-jwt-vc/src => core/src/modules/sd-jwt-vc}/__tests__/SdJwtVcModule.test.ts (100%) rename packages/{sd-jwt-vc/src => core/src/modules/sd-jwt-vc}/__tests__/SdJwtVcService.test.ts (98%) rename packages/{sd-jwt-vc/tests => core/src/modules/sd-jwt-vc/__tests__}/sdJwtVc.e2e.test.ts (90%) rename packages/{sd-jwt-vc/src => core/src/modules/sd-jwt-vc}/__tests__/sdjwtvc.fixtures.ts (100%) rename packages/{sd-jwt-vc/src => core/src/modules/sd-jwt-vc}/index.ts (100%) rename packages/{sd-jwt-vc/src => core/src/modules/sd-jwt-vc}/repository/SdJwtVcRecord.ts (80%) rename packages/{sd-jwt-vc/src => core/src/modules/sd-jwt-vc}/repository/SdJwtVcRepository.ts (54%) rename packages/{sd-jwt-vc/src => core/src/modules/sd-jwt-vc}/repository/__tests__/SdJwtVcRecord.test.ts (100%) rename packages/{sd-jwt-vc/src => core/src/modules/sd-jwt-vc}/repository/index.ts (100%) delete mode 100644 packages/sd-jwt-vc/README.md delete mode 100644 packages/sd-jwt-vc/jest.config.ts delete mode 100644 packages/sd-jwt-vc/package.json delete mode 100644 packages/sd-jwt-vc/src/SdJwtVcError.ts delete mode 100644 packages/sd-jwt-vc/tests/setup.ts delete mode 100644 packages/sd-jwt-vc/tsconfig.build.json delete mode 100644 packages/sd-jwt-vc/tsconfig.json diff --git a/demo-openid/package.json b/demo-openid/package.json index a105d429c5..e3a22d39c5 100644 --- a/demo-openid/package.json +++ b/demo-openid/package.json @@ -26,7 +26,6 @@ "@aries-framework/askar": "*", "@aries-framework/core": "*", "@aries-framework/node": "*", - "@aries-framework/sd-jwt-vc": "^0.4.2", "@types/express": "^4.17.13", "@types/figlet": "^1.5.4", "@types/inquirer": "^8.2.6", diff --git a/demo-openid/src/Holder.ts b/demo-openid/src/Holder.ts index c53050ecd5..a4867c14ca 100644 --- a/demo-openid/src/Holder.ts +++ b/demo-openid/src/Holder.ts @@ -7,7 +7,6 @@ import type { import { AskarModule } from '@aries-framework/askar' import { W3cJwtVerifiableCredential, W3cJsonLdVerifiableCredential } from '@aries-framework/core' import { OpenId4VcHolderModule } from '@aries-framework/openid4vc' -import { SdJwtVcModule } from '@aries-framework/sd-jwt-vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { BaseAgent } from './BaseAgent' @@ -17,7 +16,6 @@ function getOpenIdHolderModules() { return { askar: new AskarModule({ ariesAskar }), openId4VcHolder: new OpenId4VcHolderModule(), - sdJwtVc: new SdJwtVcModule(), } as const } @@ -58,7 +56,7 @@ export class Holder extends BaseAgent> if (credential instanceof W3cJwtVerifiableCredential || credential instanceof W3cJsonLdVerifiableCredential) { return this.agent.w3cCredentials.storeCredential({ credential }) } else { - return this.agent.modules.sdJwtVc.store(credential.compact) + return this.agent.sdJwtVc.store(credential.compact) } }) ) diff --git a/demo-openid/src/HolderInquirer.ts b/demo-openid/src/HolderInquirer.ts index b37f27fa03..a28df6a6d0 100644 --- a/demo-openid/src/HolderInquirer.ts +++ b/demo-openid/src/HolderInquirer.ts @@ -121,7 +121,7 @@ export class HolderInquirer extends BaseInquirer { credentials.map((credential) => credential.type === 'W3cCredentialRecord' ? `${credential.credential.type.join(', ')}, CredentialType: W3cVerifiableCredential` - : this.holder.agent.modules.sdJwtVc + : this.holder.agent.sdJwtVc .fromCompact(credential.compactSdJwtVc) .then((a) => `${a.prettyClaims.vct}, CredentialType: SdJwtVc`) ) diff --git a/demo-openid/src/Issuer.ts b/demo-openid/src/Issuer.ts index 203e49a322..56daaeb874 100644 --- a/demo-openid/src/Issuer.ts +++ b/demo-openid/src/Issuer.ts @@ -17,7 +17,6 @@ import { w3cDate, } from '@aries-framework/core' import { OpenId4VcIssuerModule, OpenId4VciCredentialFormatProfile } from '@aries-framework/openid4vc' -import { SdJwtVcModule } from '@aries-framework/sd-jwt-vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { Router } from 'express' @@ -109,7 +108,6 @@ function getCredentialRequestToCredentialMapper({ } export class Issuer extends BaseAgent<{ - sdJwtVc: SdJwtVcModule askar: AskarModule openId4VcIssuer: OpenId4VcIssuerModule }> { @@ -122,7 +120,6 @@ export class Issuer extends BaseAgent<{ port, name, modules: { - sdJwtVc: new SdJwtVcModule(), askar: new AskarModule({ ariesAskar }), openId4VcIssuer: new OpenId4VcIssuerModule({ baseUrl: 'http://localhost:2000/oid4vci', diff --git a/packages/core/package.json b/packages/core/package.json index e06adb7e09..b97acf99f0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -23,6 +23,7 @@ "prepublishOnly": "yarn run build" }, "dependencies": { + "@sd-jwt/core": "^0.2.0", "@digitalcredentials/jsonld": "^5.2.1", "@digitalcredentials/jsonld-signatures": "^9.3.1", "@digitalcredentials/vc": "^1.1.2", diff --git a/packages/core/src/agent/AgentModules.ts b/packages/core/src/agent/AgentModules.ts index faf87ecec7..efe603a40f 100644 --- a/packages/core/src/agent/AgentModules.ts +++ b/packages/core/src/agent/AgentModules.ts @@ -14,6 +14,7 @@ import { MessagePickupModule } from '../modules/message-pickup' import { OutOfBandModule } from '../modules/oob' import { ProofsModule } from '../modules/proofs' import { MediationRecipientModule, MediatorModule } from '../modules/routing' +import { SdJwtVcModule } from '../modules/sd-jwt-vc' import { W3cCredentialsModule } from '../modules/vc' import { WalletModule } from '../wallet' @@ -133,6 +134,7 @@ function getDefaultAgentModules() { w3cCredentials: () => new W3cCredentialsModule(), cache: () => new CacheModule(), pex: () => new DifPresentationExchangeModule(), + sdJwtVc: () => new SdJwtVcModule(), } as const } diff --git a/packages/core/src/agent/BaseAgent.ts b/packages/core/src/agent/BaseAgent.ts index f265f6418a..4cabc81211 100644 --- a/packages/core/src/agent/BaseAgent.ts +++ b/packages/core/src/agent/BaseAgent.ts @@ -18,6 +18,7 @@ import { MessagePickupApi } from '../modules/message-pickup/MessagePickupApi' import { OutOfBandApi } from '../modules/oob' import { ProofsApi } from '../modules/proofs' import { MediatorApi, MediationRecipientApi } from '../modules/routing' +import { SdJwtVcApi } from '../modules/sd-jwt-vc' import { W3cCredentialsApi } from '../modules/vc/W3cCredentialsApi' import { StorageUpdateService } from '../storage' import { UpdateAssistant } from '../storage/migration/UpdateAssistant' @@ -58,6 +59,7 @@ export abstract class BaseAgent> @@ -106,6 +108,7 @@ export abstract class BaseAgent ): Promise { - const sdJwtVc = _SdJwtVc.fromCompact(compactSdJwtVc).withHasher(this.hasher) + const sdJwtVc = _SdJwtVc.fromCompact
(compactSdJwtVc).withHasher(this.hasher) const holder = await this.extractKeyFromHolderBinding(agentContext, this.parseHolderBindingFromCredential(sdJwtVc)) // FIXME: we create the SD-JWT in two steps as the _sd_hash is currently not included in the SD-JWT library // so we add it ourselves, but for that we need the contents of the derived SD-JWT first - let compactDerivedSdJwtVc = await sdJwtVc.present(presentationFrame === true ? undefined : presentationFrame) - // FIXME: can be removed once https://github.com/berendsliedrecht/sd-jwt-ts/pull/19 is released - if (!compactDerivedSdJwtVc.endsWith('~')) { - compactDerivedSdJwtVc = `${compactDerivedSdJwtVc}~` + const compactDerivedSdJwtVc = await sdJwtVc.present(presentationFrame === true ? undefined : presentationFrame) + + let sdAlg: string + try { + sdAlg = sdJwtVc.getClaimInPayload('_sd_alg') + } catch (error) { + sdAlg = 'sha-256' } const header = { @@ -144,7 +142,7 @@ export class SdJwtVcService { // FIXME: _sd_hash is missing. See // https://github.com/berendsliedrecht/sd-jwt-ts/issues/8 - _sd_hash: TypedArrayEncoder.toBase64URL(await this.hasher.hasher(compactDerivedSdJwtVc)), + _sd_hash: TypedArrayEncoder.toBase64URL(await this.hasher.hasher(compactDerivedSdJwtVc, sdAlg)), } const compactKbJwt = await new KeyBinding({ header, payload }) @@ -179,12 +177,19 @@ export class SdJwtVcService { throw new SdJwtVcError('Keybinding is required for verification of the sd-jwt-vc') } + let sdAlg: string + try { + sdAlg = sdJwtVc.getClaimInPayload('_sd_alg') + } catch (error) { + sdAlg = 'sha-256' + } + // FIXME: Calculate _sd_hash. can be removed once below is resolved // https://github.com/berendsliedrecht/sd-jwt-ts/issues/8 const sdJwtParts = compactSdJwtVc.split('~') sdJwtParts.pop() // remove kb-jwt const sdJwtWithoutKbJwt = `${sdJwtParts.join('~')}~` - const sdHash = TypedArrayEncoder.toBase64URL(await this.hasher.hasher(sdJwtWithoutKbJwt)) + const sdHash = TypedArrayEncoder.toBase64URL(await this.hasher.hasher(sdJwtWithoutKbJwt, sdAlg)) // Assert `aud` and `nonce` claims sdJwtVc.keyBinding.assertClaimInPayload('aud', keyBinding.audience) @@ -249,7 +254,10 @@ export class SdJwtVcService { private get hasher(): HasherAndAlgorithm { return { algorithm: HasherAlgorithm.Sha256, - hasher: (input: string) => { + hasher: (input: string, algorithm) => { + if (algorithm !== 'sha-256') { + throw new SdJwtVcError(`Unsupported hashing algorithm used: ${algorithm}. Only sha-256 is supported`) + } const serializedInput = TypedArrayEncoder.fromString(input) return Hasher.hash(serializedInput, 'sha2-256') }, diff --git a/packages/sd-jwt-vc/src/__tests__/SdJwtVcModule.test.ts b/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcModule.test.ts similarity index 100% rename from packages/sd-jwt-vc/src/__tests__/SdJwtVcModule.test.ts rename to packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcModule.test.ts diff --git a/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts b/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcService.test.ts similarity index 98% rename from packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts rename to packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcService.test.ts index 4bf3dbffe5..08b58a850a 100644 --- a/packages/sd-jwt-vc/src/__tests__/SdJwtVcService.test.ts +++ b/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcService.test.ts @@ -1,7 +1,21 @@ import type { SdJwtVcHeader } from '../SdJwtVcOptions' import type { Jwk, Key } from '@aries-framework/core' -import { AskarModule } from '@aries-framework/askar' +import { AskarModule } from '../../../../../askar/src' +import { askarModuleConfig } from '../../../../../askar/tests/helpers' +import { agentDependencies } from '../../../../tests' +import { SdJwtVcService } from '../SdJwtVcService' +import { SdJwtVcRepository } from '../repository' + +import { + complexSdJwtVc, + complexSdJwtVcPresentation, + sdJwtVcWithSingleDisclosure, + sdJwtVcWithSingleDisclosurePresentation, + simpleJwtVc, + simpleJwtVcPresentation, +} from './sdjwtvc.fixtures' + import { parseDid, getJwkFromKey, @@ -14,20 +28,6 @@ import { Agent, TypedArrayEncoder, } from '@aries-framework/core' -import { agentDependencies } from '@aries-framework/node' -import { ariesAskar } from '@hyperledger/aries-askar-nodejs' - -import { SdJwtVcService } from '../SdJwtVcService' -import { SdJwtVcRepository } from '../repository' - -import { - complexSdJwtVc, - complexSdJwtVcPresentation, - sdJwtVcWithSingleDisclosure, - sdJwtVcWithSingleDisclosurePresentation, - simpleJwtVc, - simpleJwtVcPresentation, -} from './sdjwtvc.fixtures' const jwkJsonWithoutUse = (jwk: Jwk) => { const jwkJson = jwk.toJson() @@ -38,7 +38,7 @@ const jwkJsonWithoutUse = (jwk: Jwk) => { const agent = new Agent({ config: { label: 'sdjwtvcserviceagent', walletConfig: { id: utils.uuid(), key: utils.uuid() } }, modules: { - askar: new AskarModule({ ariesAskar }), + askar: new AskarModule(askarModuleConfig), dids: new DidsModule({ resolvers: [new KeyDidResolver()], registrars: [new KeyDidRegistrar()], diff --git a/packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts b/packages/core/src/modules/sd-jwt-vc/__tests__/sdJwtVc.e2e.test.ts similarity index 90% rename from packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts rename to packages/core/src/modules/sd-jwt-vc/__tests__/sdJwtVc.e2e.test.ts index d45afb35a6..50cf4ad2d6 100644 --- a/packages/sd-jwt-vc/tests/sdJwtVc.e2e.test.ts +++ b/packages/core/src/modules/sd-jwt-vc/__tests__/sdJwtVc.e2e.test.ts @@ -1,28 +1,26 @@ import type { Key } from '@aries-framework/core' -import { AskarModule } from '@aries-framework/askar' +import { AskarModule } from '../../../../../askar/src' +import { askarModuleConfig } from '../../../../../askar/tests/helpers' +import { agentDependencies } from '../../../../tests' + import { - getJwkFromKey, Agent, DidKey, DidsModule, + getJwkFromKey, KeyDidRegistrar, KeyDidResolver, KeyType, TypedArrayEncoder, utils, } from '@aries-framework/core' -import { agentDependencies } from '@aries-framework/node' -import { ariesAskar } from '@hyperledger/aries-askar-nodejs' - -import { SdJwtVcModule } from '../src' const getAgent = (label: string) => new Agent({ config: { label, walletConfig: { id: utils.uuid(), key: utils.uuid() } }, modules: { - sdJwt: new SdJwtVcModule(), - askar: new AskarModule({ ariesAskar }), + askar: new AskarModule(askarModuleConfig), dids: new DidsModule({ resolvers: [new KeyDidResolver()], registrars: [new KeyDidRegistrar()], @@ -82,7 +80,7 @@ describe('sd-jwt-vc end to end test', () => { is_over_65: true, } as const - const { compact, header, payload } = await issuer.modules.sdJwt.sign({ + const { compact, header, payload } = await issuer.sdJwtVc.sign({ payload: credential, // FIXME: sd-jwt library does not support did binding for holder yet // issuance is fine, but in verification of KB the jwk will be extracted @@ -117,7 +115,7 @@ describe('sd-jwt-vc end to end test', () => { type Header = typeof header // parse SD-JWT - const sdJwtVc = await holder.modules.sdJwt.fromCompact(compact) + const sdJwtVc = await holder.sdJwtVc.fromCompact(compact) expect(sdJwtVc).toEqual({ compact: expect.any(String), header: { @@ -189,13 +187,13 @@ describe('sd-jwt-vc end to end test', () => { }) // Verify SD-JWT (does not require key binding) - const { verification } = await holder.modules.sdJwt.verify({ + const { verification } = await holder.sdJwtVc.verify({ compactSdJwtVc: compact, }) expect(verification.isValid).toBe(true) // Store credential - await holder.modules.sdJwt.store(compact) + await holder.sdJwtVc.store(compact) // Metadata created by the verifier and send out of band by the verifier to the holder const verifierMetadata = { @@ -204,7 +202,7 @@ describe('sd-jwt-vc end to end test', () => { nonce: await verifier.wallet.generateNonce(), } - const presentation = await holder.modules.sdJwt.present({ + const presentation = await holder.sdJwtVc.present({ compactSdJwtVc: compact, verifierMetadata, presentationFrame: { @@ -226,7 +224,7 @@ describe('sd-jwt-vc end to end test', () => { }, }) - const { verification: presentationVerification } = await verifier.modules.sdJwt.verify({ + const { verification: presentationVerification } = await verifier.modules.sdJwtVc.verify({ compactSdJwtVc: presentation, keyBinding: { audience: verifierDid, nonce: verifierMetadata.nonce }, requiredClaimKeys: [ diff --git a/packages/sd-jwt-vc/src/__tests__/sdjwtvc.fixtures.ts b/packages/core/src/modules/sd-jwt-vc/__tests__/sdjwtvc.fixtures.ts similarity index 100% rename from packages/sd-jwt-vc/src/__tests__/sdjwtvc.fixtures.ts rename to packages/core/src/modules/sd-jwt-vc/__tests__/sdjwtvc.fixtures.ts diff --git a/packages/sd-jwt-vc/src/index.ts b/packages/core/src/modules/sd-jwt-vc/index.ts similarity index 100% rename from packages/sd-jwt-vc/src/index.ts rename to packages/core/src/modules/sd-jwt-vc/index.ts diff --git a/packages/sd-jwt-vc/src/repository/SdJwtVcRecord.ts b/packages/core/src/modules/sd-jwt-vc/repository/SdJwtVcRecord.ts similarity index 80% rename from packages/sd-jwt-vc/src/repository/SdJwtVcRecord.ts rename to packages/core/src/modules/sd-jwt-vc/repository/SdJwtVcRecord.ts index 13c6e93ff8..e2a77a73bb 100644 --- a/packages/sd-jwt-vc/src/repository/SdJwtVcRecord.ts +++ b/packages/core/src/modules/sd-jwt-vc/repository/SdJwtVcRecord.ts @@ -1,8 +1,13 @@ -import type { TagsBase, Constructable, JwaSignatureAlgorithm } from '@aries-framework/core' +import type { JwaSignatureAlgorithm } from '../../../crypto' +import type { TagsBase } from '../../../storage/BaseRecord' +import type { Constructable } from '../../../utils/mixins' -import { JsonTransformer, BaseRecord, utils } from '@aries-framework/core' import { SdJwtVc } from '@sd-jwt/core' +import { BaseRecord } from '../../../storage/BaseRecord' +import { JsonTransformer } from '../../../utils' +import { uuid } from '../../../utils/uuid' + export type DefaultSdJwtVcRecordTags = { vct: string @@ -37,7 +42,7 @@ export class SdJwtVcRecord extends BaseRecord { super() if (props) { - this.id = props.id ?? utils.uuid() + this.id = props.id ?? uuid() this.createdAt = props.createdAt ?? new Date() this.compactSdJwtVc = props.compactSdJwtVc this._tags = props.tags ?? {} diff --git a/packages/sd-jwt-vc/src/repository/SdJwtVcRepository.ts b/packages/core/src/modules/sd-jwt-vc/repository/SdJwtVcRepository.ts similarity index 54% rename from packages/sd-jwt-vc/src/repository/SdJwtVcRepository.ts rename to packages/core/src/modules/sd-jwt-vc/repository/SdJwtVcRepository.ts index 7ad7c2ecb8..0aa8bbce3d 100644 --- a/packages/sd-jwt-vc/src/repository/SdJwtVcRepository.ts +++ b/packages/core/src/modules/sd-jwt-vc/repository/SdJwtVcRepository.ts @@ -1,4 +1,8 @@ -import { EventEmitter, InjectionSymbols, inject, injectable, Repository, StorageService } from '@aries-framework/core' +import { EventEmitter } from '../../../agent/EventEmitter' +import { InjectionSymbols } from '../../../constants' +import { inject, injectable } from '../../../plugins' +import { Repository } from '../../../storage/Repository' +import { StorageService } from '../../../storage/StorageService' import { SdJwtVcRecord } from './SdJwtVcRecord' diff --git a/packages/sd-jwt-vc/src/repository/__tests__/SdJwtVcRecord.test.ts b/packages/core/src/modules/sd-jwt-vc/repository/__tests__/SdJwtVcRecord.test.ts similarity index 100% rename from packages/sd-jwt-vc/src/repository/__tests__/SdJwtVcRecord.test.ts rename to packages/core/src/modules/sd-jwt-vc/repository/__tests__/SdJwtVcRecord.test.ts index 622b1cca69..e288aab5cd 100644 --- a/packages/sd-jwt-vc/src/repository/__tests__/SdJwtVcRecord.test.ts +++ b/packages/core/src/modules/sd-jwt-vc/repository/__tests__/SdJwtVcRecord.test.ts @@ -1,7 +1,7 @@ -import { JsonTransformer } from '@aries-framework/core' - import { SdJwtVcRecord } from '../SdJwtVcRecord' +import { JsonTransformer } from '@aries-framework/core' + describe('SdJwtVcRecord', () => { test('sets the values passed in the constructor on the record', () => { const compactSdJwtVc = diff --git a/packages/sd-jwt-vc/src/repository/index.ts b/packages/core/src/modules/sd-jwt-vc/repository/index.ts similarity index 100% rename from packages/sd-jwt-vc/src/repository/index.ts rename to packages/core/src/modules/sd-jwt-vc/repository/index.ts diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts index 0dda7f3254..d800b626ac 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts @@ -4,8 +4,14 @@ import type { OpenId4VciCredentialSupportedWithId, OpenId4VciIssuerMetadata, } from '../shared' -import type { AgentContext, JwaSignatureAlgorithm, W3cVerifiableCredential, Key, JwkJson } from '@aries-framework/core' -import type { SdJwtVcModule, SdJwtVc } from '@aries-framework/sd-jwt-vc' +import type { + AgentContext, + JwaSignatureAlgorithm, + W3cVerifiableCredential, + Key, + JwkJson, + SdJwtVc, +} from '@aries-framework/core' import type { AccessTokenResponse, CredentialResponse, @@ -17,6 +23,7 @@ import type { } from '@sphereon/oid4vci-common' import { + SdJwtVcApi, getJwkFromJson, DidsApi, AriesFrameworkError, @@ -37,7 +44,6 @@ import { inject, injectable, parseDid, - getApiForModuleByName, } from '@aries-framework/core' import { AccessTokenClient, @@ -69,6 +75,7 @@ import { } from './OpenId4VciHolderServiceOptions' // FIXME: this is also defined in the sphereon lib, is there a reason we don't use that one? +// We use this to get PAR working and because we don't use the oid4vci client in sphereon's lib async function createAuthorizationRequestUri(options: { credentialOffer: OpenId4VciCredentialOfferPayload metadata: OpenId4VciResolvedCredentialOffer['metadata'] @@ -604,11 +611,7 @@ export class OpenId4VciHolderService { }, but the credential is not a string. ${JSON.stringify(credentialResponse.successBody.credential)}` ) - const sdJwtVcApi = getApiForModuleByName(agentContext, 'SdJwtVcModule') - if (!sdJwtVcApi) - throw new AriesFrameworkError( - `Could not find the SdJwtVcApi. Make sure the @aries-framework/sd-jwt-vc module is registered.` - ) + const sdJwtVcApi = agentContext.dependencyManager.resolve(SdJwtVcApi) const { verification, sdJwtVc } = await sdJwtVcApi.verify({ compactSdJwtVc: credentialResponse.successBody.credential, }) diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts index 550c18040a..633d0a03ab 100644 --- a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts @@ -1,7 +1,5 @@ -import type { Key } from '@aries-framework/core' -import type { SdJwtVc } from '@aries-framework/sd-jwt-vc' +import type { Key, SdJwtVc } from '@aries-framework/core' -import { AskarModule } from '@aries-framework/askar' import { getJwkFromKey, Agent, @@ -11,11 +9,11 @@ import { TypedArrayEncoder, W3cJwtVerifiableCredential, } from '@aries-framework/core' -import { agentDependencies } from '@aries-framework/node' -import { SdJwtVcModule } from '@aries-framework/sd-jwt-vc' -import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import nock, { cleanAll, enableNetConnect } from 'nock' +import { AskarModule } from '../../../../askar/src' +import { askarModuleConfig } from '../../../../askar/tests/helpers' +import { agentDependencies } from '../../../../node/src' import { OpenId4VcHolderModule } from '../OpenId4VcHolderModule' import { animoOpenIdPlaygroundDraft11SdJwtVc, matrrLaunchpadDraft11JwtVcJson, waltIdDraft11JwtVcJson } from './fixtures' @@ -28,8 +26,7 @@ const holder = new Agent({ dependencies: agentDependencies, modules: { openId4VcHolder: new OpenId4VcHolderModule(), - sdJwtVc: new SdJwtVcModule(), - askar: new AskarModule({ ariesAskar }), + askar: new AskarModule(askarModuleConfig), }, }) diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts index 5a986dc550..6030da45cc 100644 --- a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts @@ -1,5 +1,5 @@ import type { AgentType } from '../../../tests/utils' -import type { CreateProofRequestOptions } from '../../openid4vc-verifier' +import type { OpenId4VcCreateAuthorizationRequestOptions } from '../../openid4vc-verifier' import type { Express } from 'express' import type { Server } from 'http' @@ -81,18 +81,17 @@ describe('OpenId4VcHolder | OpenID4VP', () => { }) it('siop request with static metadata', async () => { - const createProofRequestOptions: CreateProofRequestOptions = { + const createProofRequestOptions: OpenId4VcCreateAuthorizationRequestOptions = { verificationMethod: verifier.verificationMethod, - holderMetadata: staticOpOpenIdConfigEdDSA, + openIdProvider: staticOpOpenIdConfigEdDSA, } //////////////////////////// RP (create request) //////////////////////////// - const { proofRequest, proofRequestMetadata } = await verifier.agent.modules.openId4VcVerifier.createProofRequest( - createProofRequestOptions - ) + const { authorizationRequestUri, metadata } = + await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest(createProofRequestOptions) //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(proofRequest) + const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// @@ -113,15 +112,15 @@ describe('OpenId4VcHolder | OpenID4VP', () => { //////////////////////////// RP (verify the response) //////////////////////////// - const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyProofResponse( + const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyAuthorizationResponse( submittedResponse ) - const { state, challenge } = proofRequestMetadata + const { state, nonce } = metadata expect(submission).toBe(undefined) expect(idTokenPayload).toBeDefined() expect(idTokenPayload.state).toMatch(state) - expect(idTokenPayload.nonce).toMatch(challenge) + expect(idTokenPayload.nonce).toMatch(nonce) await waitForMockFunction(mockFunction) expect(mockFunction).toBeCalledWith({ @@ -142,19 +141,18 @@ describe('OpenId4VcHolder | OpenID4VP', () => { .get('/.well-known/openid-configuration') .reply(200, staticOpOpenIdConfigEdDSA) - const createProofRequestOptions: CreateProofRequestOptions = { + const createProofRequestOptions: OpenId4VcCreateAuthorizationRequestOptions = { verificationMethod: verifier.verificationMethod, // TODO: if provided this way client metadata is not resolved for the verification method - holderMetadata: 'https://helloworld.com', + openIdProvider: 'https://helloworld.com', } //////////////////////////// RP (create request) //////////////////////////// - const { proofRequest, proofRequestMetadata } = await verifier.agent.modules.openId4VcVerifier.createProofRequest( - createProofRequestOptions - ) + const { authorizationRequestUri, metadata } = + await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest(createProofRequestOptions) //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(proofRequest) + const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// @@ -170,14 +168,14 @@ describe('OpenId4VcHolder | OpenID4VP', () => { //////////////////////////// RP (verify the response) //////////////////////////// - const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyProofResponse( + const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyAuthorizationResponse( submittedResponse ) - const { state, challenge } = proofRequestMetadata + const { state, nonce } = metadata expect(idTokenPayload).toBeDefined() expect(idTokenPayload.state).toMatch(state) - expect(idTokenPayload.nonce).toMatch(challenge) + expect(idTokenPayload.nonce).toMatch(nonce) await waitForMockFunction(mockFunction) expect(mockFunction).toBeCalledWith({ @@ -187,22 +185,22 @@ describe('OpenId4VcHolder | OpenID4VP', () => { }) it('resolving vp request with no credentials', async () => { - const createProofRequestOptions: CreateProofRequestOptions = { + const createProofRequestOptions: OpenId4VcCreateAuthorizationRequestOptions = { verificationMethod: verifier.verificationMethod, - holderMetadata: staticOpOpenIdConfigEdDSA, + openIdProvider: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, } - const { proofRequest } = await verifier.agent.modules.openId4VcVerifier.createProofRequest( + const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest( createProofRequestOptions ) //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(proofRequest) + const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') - expect(result.presentationSubmission.areRequirementsSatisfied).toBeFalsy() - expect(result.presentationSubmission.requirements.length).toBe(1) + expect(result.credentialsForRequest.areRequirementsSatisfied).toBeFalsy() + expect(result.credentialsForRequest.requirements.length).toBe(1) }) it('resolving vp request with wrong credentials errors', async () => { @@ -210,22 +208,22 @@ describe('OpenId4VcHolder | OpenID4VP', () => { credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), }) - const createProofRequestOptions: CreateProofRequestOptions = { + const createProofRequestOptions: OpenId4VcCreateAuthorizationRequestOptions = { verificationMethod: verifier.verificationMethod, - holderMetadata: staticOpOpenIdConfigEdDSA, + openIdProvider: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, } - const { proofRequest } = await verifier.agent.modules.openId4VcVerifier.createProofRequest( + const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest( createProofRequestOptions ) - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(proofRequest) + const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') //////////////////////////// OP (validate and parse the request) //////////////////////////// - expect(result.presentationSubmission.areRequirementsSatisfied).toBeFalsy() - expect(result.presentationSubmission.requirements.length).toBe(1) + expect(result.credentialsForRequest.areRequirementsSatisfied).toBeFalsy() + expect(result.credentialsForRequest.requirements.length).toBe(1) }) it('expect submitting a wrong submission to fail', async () => { @@ -237,19 +235,19 @@ describe('OpenId4VcHolder | OpenID4VP', () => { credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), }) - const createProofRequestOptions: CreateProofRequestOptions = { + const createProofRequestOptions: OpenId4VcCreateAuthorizationRequestOptions = { verificationMethod: verifier.verificationMethod, - holderMetadata: staticOpOpenIdConfigEdDSA, + openIdProvider: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, } - const { proofRequest: openBadge } = await verifier.agent.modules.openId4VcVerifier.createProofRequest( - createProofRequestOptions - ) - const { proofRequest: university } = await verifier.agent.modules.openId4VcVerifier.createProofRequest({ - ...createProofRequestOptions, - presentationDefinition: universityDegreePresentationDefinition, - }) + const { authorizationRequestUri: openBadge } = + await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest(createProofRequestOptions) + const { authorizationRequestUri: university } = + await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ + ...createProofRequestOptions, + presentationDefinition: universityDegreePresentationDefinition, + }) //////////////////////////// OP (validate and parse the request) //////////////////////////// @@ -260,7 +258,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { await expect( holder.agent.modules.openId4VcHolder.acceptPresentationRequest(resolvedOpenBadge.presentationRequest, { - submission: resolvedUniversityDegree.presentationSubmission, + submission: resolvedUniversityDegree.credentialsForRequest, submissionEntryIndexes: [0], }) ).rejects.toThrow() @@ -275,27 +273,27 @@ describe('OpenId4VcHolder | OpenID4VP', () => { credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), }) - const createProofRequestOptions: CreateProofRequestOptions = { + const createProofRequestOptions: OpenId4VcCreateAuthorizationRequestOptions = { verificationMethod: verifier.verificationMethod, - holderMetadata: staticOpOpenIdConfigEdDSA, + openIdProvider: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, } - const { proofRequest } = await verifier.agent.modules.openId4VcVerifier.createProofRequest( + const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest( createProofRequestOptions ) //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(proofRequest) + const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') - const { presentationRequest, presentationSubmission } = result - expect(presentationSubmission.areRequirementsSatisfied).toBeTruthy() - expect(presentationSubmission.requirements.length).toBe(1) - expect(presentationSubmission.requirements[0].needsCount).toBe(1) - expect(presentationSubmission.requirements[0].submissionEntry.length).toBe(1) - expect(presentationSubmission.requirements[0].submissionEntry[0].inputDescriptorId).toBe('OpenBadgeCredential') + const { presentationRequest, credentialsForRequest } = result + expect(credentialsForRequest.areRequirementsSatisfied).toBeTruthy() + expect(credentialsForRequest.requirements.length).toBe(1) + expect(credentialsForRequest.requirements[0].needsCount).toBe(1) + expect(credentialsForRequest.requirements[0].submissionEntry.length).toBe(1) + expect(credentialsForRequest.requirements[0].submissionEntry[0].inputDescriptorId).toBe('OpenBadgeCredential') expect(presentationRequest.presentationDefinitions[0].definition).toMatchObject(openBadgePresentationDefinition) }) @@ -309,45 +307,45 @@ describe('OpenId4VcHolder | OpenID4VP', () => { credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), }) - const createProofRequestOptions: CreateProofRequestOptions = { + const createProofRequestOptions: OpenId4VcCreateAuthorizationRequestOptions = { verificationMethod: verifier.verificationMethod, - holderMetadata: staticOpOpenIdConfigEdDSA, + openIdProvider: staticOpOpenIdConfigEdDSA, presentationDefinition: combinePresentationDefinitions([ openBadgePresentationDefinition, universityDegreePresentationDefinition, ]), } - const { proofRequest } = await verifier.agent.modules.openId4VcVerifier.createProofRequest( + const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest( createProofRequestOptions ) //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(proofRequest) + const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') - const { presentationSubmission } = result - expect(presentationSubmission.areRequirementsSatisfied).toBeTruthy() - expect(presentationSubmission.requirements.length).toBe(2) - expect(presentationSubmission.requirements[0].needsCount).toBe(1) - expect(presentationSubmission.requirements[0].submissionEntry.length).toBe(1) - expect(presentationSubmission.requirements[1].needsCount).toBe(1) - expect(presentationSubmission.requirements[1].submissionEntry.length).toBe(1) - expect(presentationSubmission.requirements[0].submissionEntry[0].inputDescriptorId).toBe('OpenBadgeCredential') - expect(presentationSubmission.requirements[1].submissionEntry[0].inputDescriptorId).toBe('UniversityDegree') + const { credentialsForRequest } = result + expect(credentialsForRequest.areRequirementsSatisfied).toBeTruthy() + expect(credentialsForRequest.requirements.length).toBe(2) + expect(credentialsForRequest.requirements[0].needsCount).toBe(1) + expect(credentialsForRequest.requirements[0].submissionEntry.length).toBe(1) + expect(credentialsForRequest.requirements[1].needsCount).toBe(1) + expect(credentialsForRequest.requirements[1].submissionEntry.length).toBe(1) + expect(credentialsForRequest.requirements[0].submissionEntry[0].inputDescriptorId).toBe('OpenBadgeCredential') + expect(credentialsForRequest.requirements[1].submissionEntry[0].inputDescriptorId).toBe('UniversityDegree') const { submittedResponse, status } = await holder.agent.modules.openId4VcHolder.acceptPresentationRequest( result.presentationRequest, { - submission: result.presentationSubmission, + submission: result.credentialsForRequest, submissionEntryIndexes: [0, 0], } ) expect(status).toBe(200) - const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyProofResponse( + const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyAuthorizationResponse( submittedResponse ) @@ -394,27 +392,27 @@ describe('OpenId4VcHolder | OpenID4VP', () => { credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), }) - const createProofRequestOptions: CreateProofRequestOptions = { + const createProofRequestOptions: OpenId4VcCreateAuthorizationRequestOptions = { verificationMethod: verifier.verificationMethod, - holderMetadata: staticOpOpenIdConfigEdDSA, + openIdProvider: staticOpOpenIdConfigEdDSA, presentationDefinition: combinePresentationDefinitions([ openBadgePresentationDefinition, universityDegreePresentationDefinition, ]), } - const { proofRequest } = await verifier.agent.modules.openId4VcVerifier.createProofRequest( + const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest( createProofRequestOptions ) //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(proofRequest) + const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') await expect( holder.agent.modules.openId4VcHolder.acceptPresentationRequest(result.presentationRequest, { - submission: result.presentationSubmission, + submission: result.credentialsForRequest, submissionEntryIndexes: [0], }) ).rejects.toThrow() @@ -425,26 +423,25 @@ describe('OpenId4VcHolder | OpenID4VP', () => { credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), }) - const createProofRequestOptions: CreateProofRequestOptions = { + const createProofRequestOptions: OpenId4VcCreateAuthorizationRequestOptions = { verificationMethod: verifier.verificationMethod, - holderMetadata: staticOpOpenIdConfigEdDSA, + openIdProvider: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, } - const { proofRequest, proofRequestMetadata } = await verifier.agent.modules.openId4VcVerifier.createProofRequest( - createProofRequestOptions - ) + const { authorizationRequestUri, metadata } = + await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest(createProofRequestOptions) //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(proofRequest) + const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) if (result.proofType === 'authentication') throw new Error('Expected a proofRequest') //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// // Select the appropriate credentials - result.presentationSubmission.requirements[0] + result.credentialsForRequest.requirements[0] - if (!result.presentationSubmission.areRequirementsSatisfied) { + if (!result.credentialsForRequest.areRequirementsSatisfied) { throw new Error('Requirements are not satisfied.') } @@ -452,7 +449,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const { submittedResponse, status } = await holder.agent.modules.openId4VcHolder.acceptPresentationRequest( result.presentationRequest, { - submission: result.presentationSubmission, + submission: result.credentialsForRequest, submissionEntryIndexes: [0], } ) @@ -462,14 +459,14 @@ describe('OpenId4VcHolder | OpenID4VP', () => { // The RP MUST validate that the aud (audience) Claim contains the value of the client_id // that the RP sent in the Authorization Request as an audience. // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. - const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyProofResponse( + const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyAuthorizationResponse( submittedResponse ) - const { state, challenge } = proofRequestMetadata + const { state, nonce } = metadata expect(idTokenPayload).toBeDefined() expect(idTokenPayload.state).toMatch(state) - expect(idTokenPayload.nonce).toMatch(challenge) + expect(idTokenPayload.nonce).toMatch(nonce) expect(submission).toBeDefined() expect(submission?.presentationDefinitions).toHaveLength(1) @@ -494,24 +491,23 @@ describe('OpenId4VcHolder | OpenID4VP', () => { credential, }) - const createProofRequestOptions: CreateProofRequestOptions = { + const createProofRequestOptions: OpenId4VcCreateAuthorizationRequestOptions = { verificationMethod: verifier.verificationMethod, - holderMetadata: staticOpOpenIdConfigEdDSA, + openIdProvider: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgeCredentialPresentationDefinitionLdpVc, } - const { proofRequest, proofRequestMetadata } = await verifier.agent.modules.openId4VcVerifier.createProofRequest( - createProofRequestOptions - ) + const { authorizationRequestUri, metadata } = + await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest(createProofRequestOptions) //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(proofRequest) + const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) if (result.proofType === 'authentication') throw new Error('Expected a proofRequest') //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// // Select the appropriate credentials - if (!result.presentationSubmission.areRequirementsSatisfied) { + if (!result.credentialsForRequest.areRequirementsSatisfied) { throw new Error('Requirements are not satisfied.') } @@ -519,7 +515,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { const { submittedResponse, status } = await holder.agent.modules.openId4VcHolder.acceptPresentationRequest( result.presentationRequest, { - submission: result.presentationSubmission, + submission: result.credentialsForRequest, submissionEntryIndexes: [0], } ) @@ -529,14 +525,14 @@ describe('OpenId4VcHolder | OpenID4VP', () => { // The RP MUST validate that the aud (audience) Claim contains the value of the client_id // that the RP sent in the Authorization Request as an audience. // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. - const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyProofResponse( + const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyAuthorizationResponse( submittedResponse ) - const { state, challenge } = proofRequestMetadata + const { state, nonce } = metadata expect(idTokenPayload).toBeDefined() expect(idTokenPayload.state).toMatch(state) - expect(idTokenPayload.nonce).toMatch(challenge) + expect(idTokenPayload.nonce).toMatch(nonce) expect(submission).toBeDefined() expect(submission?.presentationDefinitions).toHaveLength(1) @@ -564,41 +560,41 @@ describe('OpenId4VcHolder | OpenID4VP', () => { credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), }) - const createProofRequestOptions: CreateProofRequestOptions = { + const createProofRequestOptions: OpenId4VcCreateAuthorizationRequestOptions = { verificationMethod: verifier.verificationMethod, - holderMetadata: staticOpOpenIdConfigEdDSA, + openIdProvider: staticOpOpenIdConfigEdDSA, presentationDefinition: combinePresentationDefinitions([ universityDegreePresentationDefinition, openBadgeCredentialPresentationDefinitionLdpVc, ]), } - const { proofRequest } = await verifier.agent.modules.openId4VcVerifier.createProofRequest( + const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest( createProofRequestOptions ) //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(proofRequest) + const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) if (result.proofType === 'authentication') throw new Error('Expected a proofRequest') //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// // Select the appropriate credentials - if (!result.presentationSubmission.areRequirementsSatisfied) { + if (!result.credentialsForRequest.areRequirementsSatisfied) { throw new Error('Requirements are not satisfied.') } //////////////////////////// OP (accept the verified request) //////////////////////////// await expect( holder.agent.modules.openId4VcHolder.acceptPresentationRequest(result.presentationRequest, { - submission: result.presentationSubmission, + submission: result.credentialsForRequest, submissionEntryIndexes: [0, 0], }) ).rejects.toThrow() await expect( holder.agent.modules.openId4VcHolder.acceptPresentationRequest(result.presentationRequest, { - submission: result.presentationSubmission, + submission: result.credentialsForRequest, submissionEntryIndexes: [0], }) ).rejects.toThrow() diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts index 6c621c4f4e..f1fbf9b51a 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts @@ -15,7 +15,6 @@ import type { OpenId4VciCredentialSupported, } from '../shared' import type { AgentContext, DidDocument } from '@aries-framework/core' -import type { SdJwtVcModule } from '@aries-framework/sd-jwt-vc' import type { Grant, JWTVerifyCallback } from '@sphereon/oid4vci-common' import type { CredentialDataSupplier, @@ -26,11 +25,11 @@ import type { import type { ICredential } from '@sphereon/ssi-types' import { + SdJwtVcApi, AriesFrameworkError, ClaimFormat, DidsApi, equalsIgnoreOrder, - getApiForModuleByName, getJwkFromJson, getJwkFromKey, getKeyFromVerificationMethod, @@ -360,8 +359,7 @@ export class OpenId4VcIssuerService { options: OpenId4VciSignSdJwtCredential ): CredentialSignerCallback => { return async () => { - const sdJwtVcApi = getApiForModuleByName(agentContext, 'SdJwtVcModule') - if (!sdJwtVcApi) throw new AriesFrameworkError(`Could not find the SdJwtVcApi`) + const sdJwtVcApi = agentContext.dependencyManager.resolve(SdJwtVcApi) const sdJwtVc = await sdJwtVcApi.sign(options) return getSphereonVerifiableCredential(sdJwtVc) diff --git a/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.e2e.test.ts b/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.e2e.test.ts index b5b365f68a..467333efe8 100644 --- a/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.e2e.test.ts @@ -13,8 +13,8 @@ import type { } from '@aries-framework/core' import type { OriginalVerifiableCredential as SphereonW3cVerifiableCredential } from '@sphereon/ssi-types' -import { AskarModule } from '@aries-framework/askar' import { + SdJwtVcApi, JwtPayload, Agent, AriesFrameworkError, @@ -35,10 +35,10 @@ import { getKeyFromVerificationMethod, w3cDate, } from '@aries-framework/core' -import { agentDependencies } from '@aries-framework/node' -import { SdJwtVcApi, SdJwtVcModule } from '@aries-framework/sd-jwt-vc' -import { ariesAskar } from '@hyperledger/aries-askar-nodejs' +import { AskarModule } from '../../../../askar/src' +import { askarModuleConfig } from '../../../../askar/tests/helpers' +import { agentDependencies } from '../../../../node/src' import { OpenId4VciCredentialFormatProfile } from '../../shared' import { OpenId4VcIssuerModule } from '../OpenId4VcIssuerModule' import { OpenId4VcIssuerModuleConfig } from '../OpenId4VcIssuerModuleConfig' @@ -84,8 +84,7 @@ const modules = { }, }, }), - sdJwtVc: new SdJwtVcModule(), - askar: new AskarModule({ ariesAskar }), + askar: new AskarModule(askarModuleConfig), } const jwsService = new JwsService() diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierService.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierService.ts index 974f072d9b..761f8bcd30 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierService.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierService.ts @@ -268,7 +268,9 @@ export class OpenId4VcVerifierService { // FIXME: client metadata is the metadata of the RP // but it's being added as OP metadata // Is this both OP and Client metadata? - .withClientMetadata(openIdConfiguration) + // RP = Client = verifier + // OP = holder + .withClientMetadata(holderMetadata) .withCustomResolver(getResolver(agentContext)) .withResponseMode(ResponseMode.POST) .withResponseType(isVpRequest ? [ResponseType.ID_TOKEN, ResponseType.VP_TOKEN] : ResponseType.ID_TOKEN) diff --git a/packages/openid4vc/tests/openid4vc.e2e.test.ts b/packages/openid4vc/tests/openid4vc.e2e.test.ts index 1fadc089b5..93a78686c9 100644 --- a/packages/openid4vc/tests/openid4vc.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc.e2e.test.ts @@ -1,6 +1,6 @@ import type { AgentType, TenantType } from './utils' import type { OpenId4VciCredentialBindingResolver } from '../src/openid4vc-holder' -import type { SdJwtVc } from '@aries-framework/sd-jwt-vc' +import type { SdJwtVc } from '@aries-framework/core' import type { Server } from 'http' import { @@ -20,7 +20,6 @@ import express, { type Express } from 'express' import { AskarModule } from '../../askar/src' import { askarModuleConfig } from '../../askar/tests/helpers' -import { SdJwtVcModule } from '../../sd-jwt-vc/src' import { TenantsModule } from '../../tenants/src' import { OpenId4VcVerifierModule, OpenId4VcHolderModule, OpenId4VcIssuerModule } from '../src' @@ -55,8 +54,7 @@ describe('OpenId4Vc', () => { let holder: AgentType<{ openId4VcHolder: OpenId4VcHolderModule - sdJwtVc: SdJwtVcModule - tenants: TenantsModule<{ openId4VcHolder: OpenId4VcHolderModule; sdJwtVc: SdJwtVcModule }> + tenants: TenantsModule<{ openId4VcHolder: OpenId4VcHolderModule }> }> let holder1: TenantType @@ -106,7 +104,6 @@ describe('OpenId4Vc', () => { }, }, }), - sdJwtVc: new SdJwtVcModule(), askar: new AskarModule(askarModuleConfig), tenants: new TenantsModule(), }, @@ -119,7 +116,6 @@ describe('OpenId4Vc', () => { 'holder', { openId4VcHolder: new OpenId4VcHolderModule(), - sdJwtVc: new SdJwtVcModule(), askar: new AskarModule(askarModuleConfig), tenants: new TenantsModule(), }, @@ -133,7 +129,6 @@ describe('OpenId4Vc', () => { openId4VcVerifier: new OpenId4VcVerifierModule({ baseUrl: verificationBaseUrl, }), - sdJwtVc: new SdJwtVcModule(), askar: new AskarModule(askarModuleConfig), tenants: new TenantsModule(), }, @@ -235,7 +230,7 @@ describe('OpenId4Vc', () => { expect(credentialsTenant1).toHaveLength(1) const compactSdJwtVcTenant1 = (credentialsTenant1[0] as SdJwtVc).compact - const sdJwtVcTenant1 = await holderTenant1.modules.sdJwtVc.fromCompact(compactSdJwtVcTenant1) + const sdJwtVcTenant1 = await holderTenant1.sdJwtVc.fromCompact(compactSdJwtVcTenant1) expect(sdJwtVcTenant1.payload.vct).toEqual('UniversityDegreeCredential') const resolvedCredentialOffer2 = await holderTenant1.modules.openId4VcHolder.resolveCredentialOffer( @@ -261,7 +256,7 @@ describe('OpenId4Vc', () => { expect(credentialsTenant2).toHaveLength(1) const compactSdJwtVcTenant2 = (credentialsTenant2[0] as SdJwtVc).compact - const sdJwtVcTenant2 = await holderTenant1.modules.sdJwtVc.fromCompact(compactSdJwtVcTenant2) + const sdJwtVcTenant2 = await holderTenant1.sdJwtVc.fromCompact(compactSdJwtVcTenant2) expect(sdJwtVcTenant2.payload.vct).toEqual('UniversityDegreeCredential2') await holderTenant1.endSession() diff --git a/packages/sd-jwt-vc/README.md b/packages/sd-jwt-vc/README.md deleted file mode 100644 index aaaac48824..0000000000 --- a/packages/sd-jwt-vc/README.md +++ /dev/null @@ -1,57 +0,0 @@ -

-
- Hyperledger Aries logo -

-

Aries Framework JavaScript Selective Disclosure JWT VC Module

-

- License - typescript - @aries-framework/sd-jwt-vc version -

-
- -### Installation - -Add the `sd-jwt-vc` module to your project. - -```sh -yarn add @aries-framework/sd-jwt-vc -``` - -### Quick start - -After the installation you can follow the [guide to setup your agent](https://aries.js.org/guides/0.4/getting-started/set-up) and add the following to your agent modules. - -```ts -import { SdJwtVcModule } from '@aries-framework/sd-jwt-vc' - -const agent = new Agent({ - config: { - /* config */ - }, - dependencies: agentDependencies, - modules: { - sdJwtVc: new SdJwtVcModule(), - /* other custom modules */ - }, -}) - -await agent.initialize() -``` diff --git a/packages/sd-jwt-vc/jest.config.ts b/packages/sd-jwt-vc/jest.config.ts deleted file mode 100644 index 93c0197296..0000000000 --- a/packages/sd-jwt-vc/jest.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Config } from '@jest/types' - -import base from '../../jest.config.base' - -import packageJson from './package.json' - -const config: Config.InitialOptions = { - ...base, - displayName: packageJson.name, - setupFilesAfterEnv: ['./tests/setup.ts'], -} - -export default config diff --git a/packages/sd-jwt-vc/package.json b/packages/sd-jwt-vc/package.json deleted file mode 100644 index 8f0db38ec3..0000000000 --- a/packages/sd-jwt-vc/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "@aries-framework/sd-jwt-vc", - "main": "build/index", - "types": "build/index", - "version": "0.4.2", - "files": [ - "build" - ], - "license": "Apache-2.0", - "publishConfig": { - "access": "public" - }, - "homepage": "https://github.com/hyperledger/aries-framework-javascript/tree/main/packages/sd-jwt-vc", - "repository": { - "type": "git", - "url": "https://github.com/hyperledger/aries-framework-javascript", - "directory": "packages/sd-jwt-vc" - }, - "scripts": { - "build": "yarn run clean && yarn run compile", - "clean": "rimraf ./build", - "compile": "tsc -p tsconfig.build.json", - "prepublishOnly": "yarn run build", - "test": "jest" - }, - "dependencies": { - "@aries-framework/askar": "^0.4.2", - "@aries-framework/core": "^0.4.2", - "class-transformer": "0.5.1", - "class-validator": "0.14.0", - "@sd-jwt/core": "^0.2.0" - }, - "devDependencies": { - "@aries-framework/node": "^0.4.2", - "@hyperledger/aries-askar-nodejs": "^0.2.0-dev.5", - "reflect-metadata": "^0.1.13", - "rimraf": "^4.4.0", - "typescript": "~4.9.5" - } -} diff --git a/packages/sd-jwt-vc/src/SdJwtVcError.ts b/packages/sd-jwt-vc/src/SdJwtVcError.ts deleted file mode 100644 index cacc4c7511..0000000000 --- a/packages/sd-jwt-vc/src/SdJwtVcError.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { AriesFrameworkError } from '@aries-framework/core' - -export class SdJwtVcError extends AriesFrameworkError {} diff --git a/packages/sd-jwt-vc/tests/setup.ts b/packages/sd-jwt-vc/tests/setup.ts deleted file mode 100644 index 78143033f2..0000000000 --- a/packages/sd-jwt-vc/tests/setup.ts +++ /dev/null @@ -1,3 +0,0 @@ -import 'reflect-metadata' - -jest.setTimeout(120000) diff --git a/packages/sd-jwt-vc/tsconfig.build.json b/packages/sd-jwt-vc/tsconfig.build.json deleted file mode 100644 index 2b75d0adab..0000000000 --- a/packages/sd-jwt-vc/tsconfig.build.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "../../tsconfig.build.json", - "compilerOptions": { - "outDir": "./build" - }, - "include": ["src/**/*"] -} diff --git a/packages/sd-jwt-vc/tsconfig.json b/packages/sd-jwt-vc/tsconfig.json deleted file mode 100644 index 46efe6f721..0000000000 --- a/packages/sd-jwt-vc/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "types": ["jest"] - } -} From 2ae6b325871e4c9417540f3371fc3a9985649411 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Wed, 24 Jan 2024 15:44:59 +0700 Subject: [PATCH 109/115] a small bug Signed-off-by: Timo Glastra --- packages/core/src/modules/vc/models/ClaimFormat.ts | 1 + .../modules/vc/models/credential/W3cVerifiableCredential.ts | 4 ++-- .../vc/models/presentation/W3cVerifiablePresentation.ts | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/core/src/modules/vc/models/ClaimFormat.ts b/packages/core/src/modules/vc/models/ClaimFormat.ts index f6c8cc909d..50ff6c58c9 100644 --- a/packages/core/src/modules/vc/models/ClaimFormat.ts +++ b/packages/core/src/modules/vc/models/ClaimFormat.ts @@ -9,4 +9,5 @@ export enum ClaimFormat { Ldp = 'ldp', LdpVc = 'ldp_vc', LdpVp = 'ldp_vp', + SdJwtVc = 'vc+sd-jwt', } diff --git a/packages/core/src/modules/vc/models/credential/W3cVerifiableCredential.ts b/packages/core/src/modules/vc/models/credential/W3cVerifiableCredential.ts index ae16558744..f935bb08e0 100644 --- a/packages/core/src/modules/vc/models/credential/W3cVerifiableCredential.ts +++ b/packages/core/src/modules/vc/models/credential/W3cVerifiableCredential.ts @@ -39,7 +39,7 @@ export function W3cVerifiableCredentialTransformer() { export type W3cVerifiableCredential = Format extends ClaimFormat.JwtVc - ? W3cJsonLdVerifiableCredential - : Format extends ClaimFormat.LdpVc ? W3cJwtVerifiableCredential + : Format extends ClaimFormat.LdpVc + ? W3cJsonLdVerifiableCredential : W3cJsonLdVerifiableCredential | W3cJwtVerifiableCredential diff --git a/packages/core/src/modules/vc/models/presentation/W3cVerifiablePresentation.ts b/packages/core/src/modules/vc/models/presentation/W3cVerifiablePresentation.ts index 65e7b68a4b..8ce1304a19 100644 --- a/packages/core/src/modules/vc/models/presentation/W3cVerifiablePresentation.ts +++ b/packages/core/src/modules/vc/models/presentation/W3cVerifiablePresentation.ts @@ -4,7 +4,7 @@ import type { ClaimFormat } from '../ClaimFormat' export type W3cVerifiablePresentation = Format extends ClaimFormat.JwtVp - ? W3cJsonLdVerifiablePresentation - : Format extends ClaimFormat.LdpVp ? W3cJwtVerifiablePresentation + : Format extends ClaimFormat.LdpVp + ? W3cJsonLdVerifiablePresentation : W3cJsonLdVerifiablePresentation | W3cJwtVerifiablePresentation From de4e310eac133b994f5cc2fe6b25a49131d03494 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Wed, 24 Jan 2024 16:34:13 +0700 Subject: [PATCH 110/115] support sd-jwt in pex Signed-off-by: Timo Glastra --- .../DifPresentationExchangeService.ts | 382 +++++++++--------- .../models/DifPexCredentialsForRequest.ts | 7 +- .../utils/credentialSelection.ts | 57 +-- .../dif-presentation-exchange/utils/index.ts | 1 + .../utils/presentationsToCreate.ts | 89 ++++ .../utils/transform.ts | 81 +--- ...fPresentationExchangeProofFormatService.ts | 43 +- .../OpenId4VcIssuerServiceOptions.ts | 3 +- .../OpenId4VcVerifierServiceOptions.ts | 2 +- packages/openid4vc/src/shared/transform.ts | 3 +- 10 files changed, 343 insertions(+), 325 deletions(-) create mode 100644 packages/core/src/modules/dif-presentation-exchange/utils/presentationsToCreate.ts diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts index 653a6bbaf9..afb65b560a 100644 --- a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts @@ -6,21 +6,30 @@ import type { DifPresentationExchangeSubmission, DifPresentationExchangeDefinitionV2, } from './models' +import type { PresentationToCreate } from './utils' import type { AgentContext } from '../../agent' import type { Query } from '../../storage/StorageService' import type { VerificationMethod } from '../dids' -import type { W3cCredentialRecord, W3cVerifiableCredential, W3cVerifiablePresentation } from '../vc' -import type { PresentationSignCallBackParams, Validated, VerifiablePresentationResult } from '@sphereon/pex' +import type { SdJwtVc, SdJwtVcRecord } from '../sd-jwt-vc' +import type { W3cVerifiablePresentation, W3cCredentialRecord } from '../vc' +import type { W3cJsonPresentation } from '../vc/models/presentation/W3cJsonPresentation' +import type { + PresentationSignCallBackParams, + SdJwtDecodedVerifiableCredentialWithKbJwtInput, + Validated, + VerifiablePresentationResult, +} from '@sphereon/pex' import type { InputDescriptorV2, PresentationDefinitionV1 } from '@sphereon/pex-models' -import type { OriginalVerifiableCredential, OriginalVerifiablePresentation } from '@sphereon/ssi-types' +import type { W3CVerifiablePresentation } from '@sphereon/ssi-types' import { Status, PEVersion, PEX } from '@sphereon/pex' import { injectable } from 'tsyringe' import { getJwkFromKey } from '../../crypto' import { AriesFrameworkError } from '../../error' -import { JsonTransformer } from '../../utils' +import { Hasher, JsonTransformer, TypedArrayEncoder } from '../../utils' import { DidsApi, getKeyFromVerificationMethod } from '../dids' +import { SdJwtVcApi } from '../sd-jwt-vc' import { ClaimFormat, SignatureSuiteRegistry, @@ -32,35 +41,34 @@ import { import { DifPresentationExchangeError } from './DifPresentationExchangeError' import { DifPresentationExchangeSubmissionLocation } from './models' import { + getSphereonOriginalVerifiablePresentation, getCredentialsForRequest, + getPresentationsToCreate, getSphereonOriginalVerifiableCredential, - getSphereonW3cVerifiablePresentation, - getW3cVerifiablePresentationInstance, } from './utils' -export type ProofStructure = Record>> - /** * @todo create a public api for using dif presentation exchange */ @injectable() export class DifPresentationExchangeService { - private pex = new PEX() + private pex = new PEX({ + hasher: (data, alg) => { + if (alg !== 'sha-256') { + throw new AriesFrameworkError('Only sha-256 is supported as hashing algorithm.') + } + return Hasher.hash(TypedArrayEncoder.fromString(data), 'sha2-256') + }, + }) + + public constructor(private w3cCredentialService: W3cCredentialService) {} public async getCredentialsForRequest( agentContext: AgentContext, presentationDefinition: DifPresentationExchangeDefinition ): Promise { const credentialRecords = await this.queryCredentialForPresentationDefinition(agentContext, presentationDefinition) - - // FIXME: why are we resolving all created dids here? - // If we want to do this we should extract all dids from the credential records and only - // fetch the dids for the queried credential records - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - const didRecords = await didsApi.getCreatedDids() - const holderDids = didRecords.map((didRecord) => didRecord.did) - - return getCredentialsForRequest(presentationDefinition, credentialRecords, holderDids) + return getCredentialsForRequest(this.pex, presentationDefinition, credentialRecords) } /** @@ -83,7 +91,7 @@ export class DifPresentationExchangeService { } // We pick the first matching VC if we are auto-selecting - credentials[submission.inputDescriptorId].push(submission.verifiableCredentials[0].credential) + credentials[submission.inputDescriptorId].push(submission.verifiableCredentials[0]) } } @@ -108,11 +116,11 @@ export class DifPresentationExchangeService { public validatePresentation( presentationDefinition: DifPresentationExchangeDefinition, - presentation: W3cVerifiablePresentation + presentation: W3cVerifiablePresentation | SdJwtVc ) { const { errors } = this.pex.evaluatePresentation( presentationDefinition, - presentation.encoded as OriginalVerifiablePresentation + getSphereonOriginalVerifiablePresentation(presentation) ) if (errors) { @@ -131,107 +139,6 @@ export class DifPresentationExchangeService { .filter((r): r is string => Boolean(r)) } - /** - * Queries the wallet for credentials that match the given presentation definition. This only does an initial query based on the - * schema of the input descriptors. It does not do any further filtering based on the constraints in the input descriptors. - */ - private async queryCredentialForPresentationDefinition( - agentContext: AgentContext, - presentationDefinition: DifPresentationExchangeDefinition - ): Promise> { - const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) - const query: Array> = [] - const presentationDefinitionVersion = PEX.definitionVersionDiscovery(presentationDefinition) - - if (!presentationDefinitionVersion.version) { - throw new DifPresentationExchangeError( - `Unable to determine the Presentation Exchange version from the presentation definition - `, - presentationDefinitionVersion.error ? { additionalMessages: [presentationDefinitionVersion.error] } : {} - ) - } - - if (presentationDefinitionVersion.version === PEVersion.v1) { - const pd = presentationDefinition as PresentationDefinitionV1 - - // The schema.uri can contain either an expanded type, or a context uri - for (const inputDescriptor of pd.input_descriptors) { - for (const schema of inputDescriptor.schema) { - query.push({ - $or: [{ expandedType: [schema.uri] }, { contexts: [schema.uri] }, { type: [schema.uri] }], - }) - } - } - } else if (presentationDefinitionVersion.version === PEVersion.v2) { - // FIXME: As PE version 2 does not have the `schema` anymore, we can't query by schema anymore. - // For now we retrieve ALL credentials, as we did the same for V1 with JWT credentials. We probably need - // to find some way to do initial filtering, hopefully if there's a filter on the `type` field or something. - } else { - throw new DifPresentationExchangeError( - `Unsupported presentation definition version ${presentationDefinitionVersion.version as unknown as string}` - ) - } - - // query the wallet ourselves first to avoid the need to query the pex library for all - // credentials for every proof request - const credentialRecords = - query.length > 0 - ? await w3cCredentialRepository.findByQuery(agentContext, { - $or: query, - }) - : await w3cCredentialRepository.getAll(agentContext) - - return credentialRecords - } - - private addCredentialToSubjectInputDescriptor( - subjectsToInputDescriptors: ProofStructure, - subjectId: string, - inputDescriptorId: string, - credential: W3cVerifiableCredential - ) { - const inputDescriptorsToCredentials = subjectsToInputDescriptors[subjectId] ?? {} - const credentials = inputDescriptorsToCredentials[inputDescriptorId] ?? [] - - credentials.push(credential) - inputDescriptorsToCredentials[inputDescriptorId] = credentials - subjectsToInputDescriptors[subjectId] = inputDescriptorsToCredentials - } - - private getPresentationFormat( - presentationDefinition: DifPresentationExchangeDefinition, - credentials: Array - ): ClaimFormat.JwtVp | ClaimFormat.LdpVp { - const allCredentialsAreJwtVc = credentials?.every((c) => typeof c === 'string') - const allCredentialsAreLdpVc = credentials?.every((c) => typeof c !== 'string') - - const inputDescriptorsNotSupportingJwtVc = ( - presentationDefinition.input_descriptors as Array - ).filter((d) => d.format && d.format.jwt_vc === undefined) - - const inputDescriptorsNotSupportingLdpVc = ( - presentationDefinition.input_descriptors as Array - ).filter((d) => d.format && d.format.ldp_vc === undefined) - - if ( - allCredentialsAreJwtVc && - (presentationDefinition.format === undefined || presentationDefinition.format.jwt_vc) && - inputDescriptorsNotSupportingJwtVc.length === 0 - ) { - return ClaimFormat.JwtVp - } else if ( - allCredentialsAreLdpVc && - (presentationDefinition.format === undefined || presentationDefinition.format.ldp_vc) && - inputDescriptorsNotSupportingLdpVc.length === 0 - ) { - return ClaimFormat.LdpVp - } else { - throw new DifPresentationExchangeError( - 'No suitable presentation format found for the given presentation definition, and credentials' - ) - } - } - public async createPresentation( agentContext: AgentContext, options: { @@ -241,85 +148,63 @@ export class DifPresentationExchangeService { * Defaults to {@link DifPresentationExchangeSubmissionLocation.PRESENTATION} */ presentationSubmissionLocation?: DifPresentationExchangeSubmissionLocation - challenge?: string + challenge: string domain?: string - nonce?: string } ) { - const { presentationDefinition, challenge, nonce, domain, presentationSubmissionLocation } = options - - const proofStructure: ProofStructure = {} - - Object.entries(options.credentialsForInputDescriptor).forEach(([inputDescriptorId, credentials]) => { - credentials.forEach((credential) => { - const subjectId = credential.credentialSubjectIds[0] - if (!subjectId) { - throw new DifPresentationExchangeError('Missing required credential subject for creating the presentation.') - } - - this.addCredentialToSubjectInputDescriptor(proofStructure, subjectId, inputDescriptorId, credential) - }) - }) + const { presentationDefinition, domain, challenge, presentationSubmissionLocation } = options const verifiablePresentationResultsWithFormat: Array<{ verifiablePresentationResult: VerifiablePresentationResult - format: ClaimFormat.LdpVp | ClaimFormat.JwtVp + claimFormat: PresentationToCreate['claimFormat'] }> = [] - const subjectToInputDescriptors = Object.entries(proofStructure) - for (const [subjectId, subjectInputDescriptorsToCredentials] of subjectToInputDescriptors) { - // Determine a suitable verification method for the presentation - const verificationMethod = await this.getVerificationMethodForSubjectId(agentContext, subjectId) - - if (!verificationMethod) { - throw new DifPresentationExchangeError(`No verification method found for subject id '${subjectId}'.`) - } - + const presentationsToCreate = getPresentationsToCreate(options.credentialsForInputDescriptor) + for (const presentationToCreate of presentationsToCreate) { // We create a presentation for each subject // Thus for each subject we need to filter all the related input descriptors and credentials // FIXME: cast to V1, as tsc errors for strange reasons if not - const inputDescriptorsForSubject = (presentationDefinition as PresentationDefinitionV1).input_descriptors.filter( - (inputDescriptor) => inputDescriptor.id in subjectInputDescriptorsToCredentials + const inputDescriptorIds = presentationToCreate.verifiableCredentials.map((c) => c.inputDescriptorId) + const inputDescriptorsForPresentation = ( + presentationDefinition as PresentationDefinitionV1 + ).input_descriptors.filter((inputDescriptor) => inputDescriptorIds.includes(inputDescriptor.id)) + + // Get all the credentials for the presentation + const credentialsForPresentation = presentationToCreate.verifiableCredentials.map((c) => + getSphereonOriginalVerifiableCredential(c.credential) ) - // Get all the credentials associated with the input descriptors - const credentialsForSubject = Object.values(subjectInputDescriptorsToCredentials) - .flat() - .map(getSphereonOriginalVerifiableCredential) - const presentationDefinitionForSubject: DifPresentationExchangeDefinition = { ...presentationDefinition, - input_descriptors: inputDescriptorsForSubject, + input_descriptors: inputDescriptorsForPresentation, // We remove the submission requirements, as it will otherwise fail to create the VP submission_requirements: undefined, } - const format = this.getPresentationFormat(presentationDefinitionForSubject, credentialsForSubject) - - // FIXME: Q1: is holder always subject id, what if there are multiple subjects??? - // FIXME: Q2: What about proofType, proofPurpose verification method for multiple subjects? const verifiablePresentationResult = await this.pex.verifiablePresentationFrom( presentationDefinitionForSubject, - credentialsForSubject, - this.getPresentationSignCallback(agentContext, verificationMethod, format), + credentialsForPresentation, + this.getPresentationSignCallback(agentContext, presentationToCreate), { - holderDID: subjectId, - proofOptions: { challenge, domain, nonce }, - signatureOptions: { verificationMethod: verificationMethod?.id }, + proofOptions: { domain, challenge }, + signatureOptions: {}, presentationSubmissionLocation: presentationSubmissionLocation ?? DifPresentationExchangeSubmissionLocation.PRESENTATION, } ) - verifiablePresentationResultsWithFormat.push({ verifiablePresentationResult, format }) + verifiablePresentationResultsWithFormat.push({ + verifiablePresentationResult, + claimFormat: presentationToCreate.claimFormat, + }) } - if (!verifiablePresentationResultsWithFormat[0]) { + if (verifiablePresentationResultsWithFormat.length === 0) { throw new DifPresentationExchangeError('No verifiable presentations created') } - if (subjectToInputDescriptors.length !== verifiablePresentationResultsWithFormat.length) { + if (presentationsToCreate.length !== verifiablePresentationResultsWithFormat.length) { throw new DifPresentationExchangeError('Invalid amount of verifiable presentations created') } @@ -336,8 +221,9 @@ export class DifPresentationExchangeService { } return { - verifiablePresentations: verifiablePresentationResultsWithFormat.map((r) => - getW3cVerifiablePresentationInstance(r.verifiablePresentationResult.verifiablePresentation) + verifiablePresentations: verifiablePresentationResultsWithFormat.map( + // Can be JSON or string (sd-jwt / jwt compact) + (r) => r.verifiablePresentationResult.verifiablePresentation as W3cJsonPresentation | string ), presentationSubmission, presentationSubmissionLocation: @@ -479,52 +365,82 @@ export class DifPresentationExchangeService { return supportedSignatureSuites[0].proofType } - public getPresentationSignCallback( - agentContext: AgentContext, - verificationMethod: VerificationMethod, - vpFormat: ClaimFormat.LdpVp | ClaimFormat.JwtVp - ) { - const w3cCredentialService = agentContext.dependencyManager.resolve(W3cCredentialService) - + private getPresentationSignCallback(agentContext: AgentContext, presentationToCreate: PresentationToCreate) { return async (callBackParams: PresentationSignCallBackParams) => { // The created partial proof and presentation, as well as original supplied options - const { presentation: presentationJson, options, presentationDefinition } = callBackParams - const { challenge, domain, nonce } = options.proofOptions ?? {} - const { verificationMethod: verificationMethodId } = options.signatureOptions ?? {} + const { presentation: presentationInput, options, presentationDefinition } = callBackParams + const { challenge, domain } = options.proofOptions ?? {} - if (verificationMethodId && verificationMethodId !== verificationMethod.id) { - throw new DifPresentationExchangeError( - `Verification method from signing options ${verificationMethodId} does not match verification method ${verificationMethod.id}` - ) + if (!challenge) { + throw new AriesFrameworkError('challenge MUST be provided when signing a VP') } - let signedPresentation: W3cVerifiablePresentation - if (vpFormat === 'jwt_vp') { - signedPresentation = await w3cCredentialService.signPresentation(agentContext, { + if (presentationToCreate.claimFormat === ClaimFormat.JwtVp) { + // Determine a suitable verification method for the presentation + const verificationMethod = await this.getVerificationMethodForSubjectId( + agentContext, + presentationToCreate.subjectIds[0] + ) + + const signedPresentation = await this.w3cCredentialService.signPresentation(agentContext, { format: ClaimFormat.JwtVp, alg: this.getSigningAlgorithmForJwtVc(presentationDefinition, verificationMethod), verificationMethod: verificationMethod.id, - presentation: JsonTransformer.fromJSON(presentationJson, W3cPresentation), - challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), + presentation: JsonTransformer.fromJSON(presentationInput, W3cPresentation), + challenge, domain, }) - } else if (vpFormat === 'ldp_vp') { - signedPresentation = await w3cCredentialService.signPresentation(agentContext, { + + return signedPresentation.encoded as W3CVerifiablePresentation + } else if (presentationToCreate.claimFormat === ClaimFormat.LdpVp) { + // Determine a suitable verification method for the presentation + const verificationMethod = await this.getVerificationMethodForSubjectId( + agentContext, + presentationToCreate.subjectIds[0] + ) + + const signedPresentation = await this.w3cCredentialService.signPresentation(agentContext, { format: ClaimFormat.LdpVp, + // TODO: we should move the check for which proof to use for a presentation to earlier + // as then we know when determining which VPs to submit already if the proof types are supported + // by the verifier, and we can then just add this to the vpToCreate interface proofType: this.getProofTypeForLdpVc(agentContext, presentationDefinition, verificationMethod), proofPurpose: 'authentication', verificationMethod: verificationMethod.id, - presentation: JsonTransformer.fromJSON(presentationJson, W3cPresentation), - challenge: challenge ?? nonce ?? (await agentContext.wallet.generateNonce()), + presentation: JsonTransformer.fromJSON(presentationInput, W3cPresentation), + challenge, domain, }) + + return signedPresentation.encoded as W3CVerifiablePresentation + } else if (presentationToCreate.claimFormat === ClaimFormat.SdJwtVc) { + const sdJwtInput = presentationInput as SdJwtDecodedVerifiableCredentialWithKbJwtInput + + if (!domain) { + throw new AriesFrameworkError( + "Missing 'domain' property, unable to set required 'aud' property in SD-JWT KB-JWT" + ) + } + + const sdJwtVcApi = this.getSdJwtVcApi(agentContext) + const sdJwtVc = await sdJwtVcApi.present({ + compactSdJwtVc: sdJwtInput.compactSdJwtVc, + // SD is already handled by PEX + presentationFrame: true, + verifierMetadata: { + audience: domain, + nonce: challenge, + // TODO: we should make this optional + issuedAt: Math.floor(Date.now() / 1000), + }, + }) + + return sdJwtVc } else { throw new DifPresentationExchangeError( - `Only JWT credentials or JSONLD credentials are supported for a single presentation` + `Only JWT, SD-JWT-VC, JSONLD credentials are supported for a single presentation` ) } - - return getSphereonW3cVerifiablePresentation(signedPresentation) } } @@ -554,4 +470,78 @@ export class DifPresentationExchangeService { return verificationMethod } + + /** + * Queries the wallet for credentials that match the given presentation definition. This only does an initial query based on the + * schema of the input descriptors. It does not do any further filtering based on the constraints in the input descriptors. + */ + private async queryCredentialForPresentationDefinition( + agentContext: AgentContext, + presentationDefinition: DifPresentationExchangeDefinition + ): Promise> { + const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) + const w3cQuery: Array> = [] + const sdJwtVcQuery: Array> = [] + const presentationDefinitionVersion = PEX.definitionVersionDiscovery(presentationDefinition) + + if (!presentationDefinitionVersion.version) { + throw new DifPresentationExchangeError( + `Unable to determine the Presentation Exchange version from the presentation definition`, + presentationDefinitionVersion.error ? { additionalMessages: [presentationDefinitionVersion.error] } : {} + ) + } + + // FIXME: in the query we should take into account the supported proof types of the verifier + // this could help enormously in the amount of credentials we have to retrieve from storage. + // NOTE: for now we don't support SD-JWT for v1, as I don't know what the schema.uri should be? + if (presentationDefinitionVersion.version === PEVersion.v1) { + const pd = presentationDefinition as PresentationDefinitionV1 + + // The schema.uri can contain either an expanded type, or a context uri + for (const inputDescriptor of pd.input_descriptors) { + for (const schema of inputDescriptor.schema) { + w3cQuery.push({ + $or: [{ expandedType: [schema.uri] }, { contexts: [schema.uri] }, { type: [schema.uri] }], + }) + } + } + } else if (presentationDefinitionVersion.version === PEVersion.v2) { + // FIXME: As PE version 2 does not have the `schema` anymore, we can't query by schema anymore. + // For now we retrieve ALL credentials, as we did the same for V1 with JWT credentials. We probably need + // to find some way to do initial filtering, hopefully if there's a filter on the `type` field or something. + } else { + throw new DifPresentationExchangeError( + `Unsupported presentation definition version ${presentationDefinitionVersion.version as unknown as string}` + ) + } + + const allRecords: Array = [] + + // query the wallet ourselves first to avoid the need to query the pex library for all + // credentials for every proof request + const w3cCredentialRecords = + w3cQuery.length > 0 + ? await w3cCredentialRepository.findByQuery(agentContext, { + $or: w3cQuery, + }) + : await w3cCredentialRepository.getAll(agentContext) + + allRecords.push(...w3cCredentialRecords) + + const sdJwtVcApi = this.getSdJwtVcApi(agentContext) + const sdJwtVcRecords = + sdJwtVcQuery.length > 0 + ? await sdJwtVcApi.findAllByQuery({ + $or: sdJwtVcQuery, + }) + : await sdJwtVcApi.getAll() + + allRecords.push(...sdJwtVcRecords) + + return allRecords + } + + private getSdJwtVcApi(agentContext: AgentContext) { + return agentContext.dependencyManager.resolve(SdJwtVcApi) + } } diff --git a/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts b/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts index ec2e83d17e..9ded2b1688 100644 --- a/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts +++ b/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts @@ -1,4 +1,5 @@ -import type { W3cCredentialRecord, W3cVerifiableCredential } from '../../vc' +import type { SdJwtVcRecord } from '../../sd-jwt-vc' +import type { W3cCredentialRecord } from '../../vc' export interface DifPexCredentialsForRequest { /** @@ -110,10 +111,10 @@ export interface DifPexCredentialsForRequestSubmissionEntry { * If the value is an empty list, it means the input descriptor could * not be satisfied. */ - verifiableCredentials: W3cCredentialRecord[] + verifiableCredentials: Array } /** * Mapping of selected credentials for an input descriptor */ -export type DifPexInputDescriptorToCredentials = Record> +export type DifPexInputDescriptorToCredentials = Record> diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts index 1fca34b943..ce0975594f 100644 --- a/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts +++ b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts @@ -1,13 +1,13 @@ +import type { SdJwtVcRecord } from '../../sd-jwt-vc' import type { W3cCredentialRecord } from '../../vc' import type { DifPexCredentialsForRequest, DifPexCredentialsForRequestRequirement, DifPexCredentialsForRequestSubmissionEntry, } from '../models' -import type { IPresentationDefinition, SelectResults, SubmissionRequirementMatch } from '@sphereon/pex' +import type { IPresentationDefinition, SelectResults, SubmissionRequirementMatch, PEX } from '@sphereon/pex' import type { InputDescriptorV1, InputDescriptorV2, SubmissionRequirement } from '@sphereon/pex-models' -import { PEX } from '@sphereon/pex' import { Rules } from '@sphereon/pex-models' import { default as jp } from 'jsonpath' @@ -17,40 +17,25 @@ import { DifPresentationExchangeError } from '../DifPresentationExchangeError' import { getSphereonOriginalVerifiableCredential } from './transform' export async function getCredentialsForRequest( + // PEX instance with hasher defined + pex: PEX, presentationDefinition: IPresentationDefinition, - credentialRecords: Array, - holderDIDs: Array + credentialRecords: Array ): Promise { - if (!presentationDefinition) { - throw new DifPresentationExchangeError('Presentation Definition is required to select credentials for submission.') - } - - const pex = new PEX() - - const encodedCredentials = credentialRecords.map((c) => getSphereonOriginalVerifiableCredential(c.credential)) - - // FIXME: there is a function for this in the VP library, but it is not usable atm - const selectResultsRaw = pex.selectFrom(presentationDefinition, encodedCredentials, { - holderDIDs, - // limitDisclosureSignatureSuites: [], - // restrictToDIDMethods, - // restrictToFormats - }) + const encodedCredentials = credentialRecords.map((c) => getSphereonOriginalVerifiableCredential(c)) + const selectResultsRaw = pex.selectFrom(presentationDefinition, encodedCredentials) const selectResults = { ...selectResultsRaw, // Map the encoded credential to their respective w3c credential record - verifiableCredential: selectResultsRaw.verifiableCredential?.map((encoded) => { - const credentialRecord = credentialRecords.find((record) => { - const originalVc = getSphereonOriginalVerifiableCredential(record.credential) - return deepEquality(originalVc, encoded) - }) + verifiableCredential: selectResultsRaw.verifiableCredential?.map((selectedEncoded) => { + const credentialRecordIndex = encodedCredentials.findIndex((encoded) => deepEquality(selectedEncoded, encoded)) - if (!credentialRecord) { + if (credentialRecordIndex === -1) { throw new DifPresentationExchangeError('Unable to find credential in credential records.') } - return credentialRecord + return credentialRecords[credentialRecordIndex] }), } @@ -95,7 +80,7 @@ export async function getCredentialsForRequest( function getSubmissionRequirements( presentationDefinition: IPresentationDefinition, - selectResults: W3cCredentialRecordSelectResults + selectResults: CredentialRecordSelectResults ): Array { const submissionRequirements: Array = [] @@ -141,7 +126,7 @@ function getSubmissionRequirements( function getSubmissionRequirementsForAllInputDescriptors( inputDescriptors: Array | Array, - selectResults: W3cCredentialRecordSelectResults + selectResults: CredentialRecordSelectResults ): Array { const submissionRequirements: Array = [] @@ -162,7 +147,7 @@ function getSubmissionRequirementsForAllInputDescriptors( function getSubmissionRequirementRuleAll( submissionRequirement: SubmissionRequirement, presentationDefinition: IPresentationDefinition, - selectResults: W3cCredentialRecordSelectResults + selectResults: CredentialRecordSelectResults ) { // Check if there's a 'from'. If not the structure is not as we expect it if (!submissionRequirement.from) @@ -201,7 +186,7 @@ function getSubmissionRequirementRuleAll( function getSubmissionRequirementRulePick( submissionRequirement: SubmissionRequirement, presentationDefinition: IPresentationDefinition, - selectResults: W3cCredentialRecordSelectResults + selectResults: CredentialRecordSelectResults ) { // Check if there's a 'from'. If not the structure is not as we expect it if (!submissionRequirement.from) { @@ -257,7 +242,7 @@ function getSubmissionRequirementRulePick( function getSubmissionForInputDescriptor( inputDescriptor: InputDescriptorV1 | InputDescriptorV2, - selectResults: W3cCredentialRecordSelectResults + selectResults: CredentialRecordSelectResults ): DifPexCredentialsForRequestSubmissionEntry { // https://github.com/Sphereon-Opensource/PEX/issues/116 // If the input descriptor doesn't contain a name, the name of the match will be the id of the input descriptor that satisfied it @@ -292,9 +277,9 @@ function getSubmissionForInputDescriptor( function extractCredentialsFromMatch( match: SubmissionRequirementMatch, - availableCredentials?: Array + availableCredentials?: Array ) { - const verifiableCredentials: Array = [] + const verifiableCredentials: Array = [] for (const vcPath of match.vc_path) { const [verifiableCredential] = jp.query({ verifiableCredential: availableCredentials }, vcPath) as [ @@ -307,8 +292,8 @@ function extractCredentialsFromMatch( } /** - * Custom SelectResults that include the W3cCredentialRecord instead of the encoded verifiable credential + * Custom SelectResults that includes the AFJ records instead of the encoded verifiable credential */ -export type W3cCredentialRecordSelectResults = Omit & { - verifiableCredential?: Array +type CredentialRecordSelectResults = Omit & { + verifiableCredential?: Array } diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/index.ts b/packages/core/src/modules/dif-presentation-exchange/utils/index.ts index aaf44fa1b6..18fe3ad53c 100644 --- a/packages/core/src/modules/dif-presentation-exchange/utils/index.ts +++ b/packages/core/src/modules/dif-presentation-exchange/utils/index.ts @@ -1,2 +1,3 @@ export * from './transform' export * from './credentialSelection' +export * from './presentationsToCreate' diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/presentationsToCreate.ts b/packages/core/src/modules/dif-presentation-exchange/utils/presentationsToCreate.ts new file mode 100644 index 0000000000..47cb5202ca --- /dev/null +++ b/packages/core/src/modules/dif-presentation-exchange/utils/presentationsToCreate.ts @@ -0,0 +1,89 @@ +import type { SdJwtVcRecord } from '../../sd-jwt-vc' +import type { DifPexInputDescriptorToCredentials } from '../models' + +import { W3cCredentialRecord, ClaimFormat } from '../../vc' +import { DifPresentationExchangeError } from '../DifPresentationExchangeError' + +// - the credentials included in the presentation +export interface SdJwtVcPresentationToCreate { + claimFormat: ClaimFormat.SdJwtVc + subjectIds: [] // subject is included in the cnf of the sd-jwt and automatically extracted by PEX + verifiableCredentials: [ + { + credential: SdJwtVcRecord + inputDescriptorId: string + } + ] // only one credential supported for SD-JWT-VC +} + +export interface JwtVpPresentationToCreate { + claimFormat: ClaimFormat.JwtVp + subjectIds: [string] // only one subject id supported for JWT VP + verifiableCredentials: Array<{ + credential: W3cCredentialRecord + inputDescriptorId: string + }> // multiple credentials supported for JWT VP +} + +export interface LdpVpPresentationToCreate { + claimFormat: ClaimFormat.LdpVp + // NOTE: we only support one subject id at the moment as we don't have proper + // support yet for adding multiple proofs to an LDP-VP + subjectIds: [string] + verifiableCredentials: Array<{ + credential: W3cCredentialRecord + inputDescriptorId: string + }> // multiple credentials supported for LDP VP +} + +export type PresentationToCreate = SdJwtVcPresentationToCreate | JwtVpPresentationToCreate | LdpVpPresentationToCreate + +// FIXME: we should extract supported format form top-level presentation definition, and input_descriptor as well +// to make sure the presentation we are going to create is a presentation format supported by the verifier. +// In addition we should allow to pass an override 'format' object, as specification like OID4VP do not use the +// PD formats, but define their own. +export function getPresentationsToCreate(credentialsForInputDescriptor: DifPexInputDescriptorToCredentials) { + const presentationsToCreate: Array = [] + + // We map all credentials for a input descriptor to the different subject ids. Each subjectId will need + // to create a separate proof (either on the same presentation or if not allowed by proof format on separate) + // presentations + for (const [inputDescriptorId, credentials] of Object.entries(credentialsForInputDescriptor)) { + for (const credential of credentials) { + if (credential instanceof W3cCredentialRecord) { + const subjectId = credential.credential.credentialSubjectIds[0] + if (!subjectId) { + throw new DifPresentationExchangeError('Missing required credential subject for creating the presentation.') + } + + // NOTE: we only support one subjectId per VP -- once we have proper support + // for multiple proofs on an LDP-VP we can add multiple subjectIds to a single VP for LDP-vp only + const expectedClaimFormat = + credential.credential.claimFormat === ClaimFormat.LdpVc ? ClaimFormat.LdpVp : ClaimFormat.JwtVp + const matchingClaimFormatAndSubject = presentationsToCreate.find( + (p): p is JwtVpPresentationToCreate => + p.claimFormat === expectedClaimFormat && p.subjectIds.includes(subjectId) + ) + + if (matchingClaimFormatAndSubject) { + matchingClaimFormatAndSubject.verifiableCredentials.push({ inputDescriptorId, credential }) + } else { + presentationsToCreate.push({ + claimFormat: expectedClaimFormat, + subjectIds: [subjectId], + verifiableCredentials: [{ credential, inputDescriptorId }], + }) + } + } else { + // SD-JWT-VC always needs it's own presentation + presentationsToCreate.push({ + claimFormat: ClaimFormat.SdJwtVc, + subjectIds: [], + verifiableCredentials: [{ inputDescriptorId, credential }], + }) + } + } + } + + return presentationsToCreate +} diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts b/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts index e4d5f694c9..4feb750477 100644 --- a/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts +++ b/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts @@ -1,78 +1,31 @@ -import type { W3cVerifiableCredential, W3cVerifiablePresentation } from '../../vc' +import type { SdJwtVcRecord, SdJwtVc } from '../../sd-jwt-vc' +import type { W3cVerifiablePresentation } from '../../vc' import type { OriginalVerifiableCredential as SphereonOriginalVerifiableCredential, - W3CVerifiableCredential as SphereonW3cVerifiableCredential, - W3CVerifiablePresentation as SphereonW3cVerifiablePresentation, + OriginalVerifiablePresentation as SphereonOriginalVerifiablePresentation, } from '@sphereon/ssi-types' -import { JsonTransformer } from '../../../utils' -import { - W3cJsonLdVerifiableCredential, - W3cJsonLdVerifiablePresentation, - W3cJwtVerifiableCredential, - W3cJwtVerifiablePresentation, - ClaimFormat, -} from '../../vc' -import { DifPresentationExchangeError } from '../DifPresentationExchangeError' +import { W3cCredentialRecord, W3cJsonLdVerifiablePresentation, W3cJwtVerifiablePresentation } from '../../vc' export function getSphereonOriginalVerifiableCredential( - w3cVerifiableCredential: W3cVerifiableCredential + credentialRecord: W3cCredentialRecord | SdJwtVcRecord ): SphereonOriginalVerifiableCredential { - if (w3cVerifiableCredential.claimFormat === ClaimFormat.LdpVc) { - return JsonTransformer.toJSON(w3cVerifiableCredential) as SphereonOriginalVerifiableCredential - } else if (w3cVerifiableCredential.claimFormat === ClaimFormat.JwtVc) { - return w3cVerifiableCredential.serializedJwt + if (credentialRecord instanceof W3cCredentialRecord) { + return credentialRecord.credential.encoded as SphereonOriginalVerifiableCredential } else { - throw new DifPresentationExchangeError( - `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` - ) + return credentialRecord.compactSdJwtVc } } -export function getSphereonW3cVerifiableCredential( - w3cVerifiableCredential: W3cVerifiableCredential -): SphereonW3cVerifiableCredential { - if (w3cVerifiableCredential.claimFormat === ClaimFormat.LdpVc) { - return JsonTransformer.toJSON(w3cVerifiableCredential) as SphereonW3cVerifiableCredential - } else if (w3cVerifiableCredential.claimFormat === ClaimFormat.JwtVc) { - return w3cVerifiableCredential.serializedJwt +export function getSphereonOriginalVerifiablePresentation( + verifiablePresentation: W3cVerifiablePresentation | SdJwtVc +): SphereonOriginalVerifiablePresentation { + if ( + verifiablePresentation instanceof W3cJwtVerifiablePresentation || + verifiablePresentation instanceof W3cJsonLdVerifiablePresentation + ) { + return verifiablePresentation.encoded as SphereonOriginalVerifiablePresentation } else { - throw new DifPresentationExchangeError( - `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` - ) - } -} - -export function getSphereonW3cVerifiablePresentation( - w3cVerifiablePresentation: W3cVerifiablePresentation -): SphereonW3cVerifiablePresentation { - if (w3cVerifiablePresentation instanceof W3cJsonLdVerifiablePresentation) { - return JsonTransformer.toJSON(w3cVerifiablePresentation) as SphereonW3cVerifiablePresentation - } else if (w3cVerifiablePresentation instanceof W3cJwtVerifiablePresentation) { - return w3cVerifiablePresentation.serializedJwt - } else { - throw new DifPresentationExchangeError( - `Unsupported claim format. Only ${ClaimFormat.LdpVc} and ${ClaimFormat.JwtVc} are supported.` - ) - } -} - -export function getW3cVerifiablePresentationInstance( - w3cVerifiablePresentation: SphereonW3cVerifiablePresentation -): W3cVerifiablePresentation { - if (typeof w3cVerifiablePresentation === 'string') { - return W3cJwtVerifiablePresentation.fromSerializedJwt(w3cVerifiablePresentation) - } else { - return JsonTransformer.fromJSON(w3cVerifiablePresentation, W3cJsonLdVerifiablePresentation) - } -} - -export function getW3cVerifiableCredentialInstance( - w3cVerifiableCredential: SphereonW3cVerifiableCredential -): W3cVerifiableCredential { - if (typeof w3cVerifiableCredential === 'string') { - return W3cJwtVerifiableCredential.fromSerializedJwt(w3cVerifiableCredential) - } else { - return JsonTransformer.fromJSON(w3cVerifiableCredential, W3cJsonLdVerifiableCredential) + return verifiablePresentation.compact } } diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts index 3260e806d2..fd99ea9944 100644 --- a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts @@ -28,7 +28,10 @@ import type { import { Attachment, AttachmentData } from '../../../../decorators/attachment/Attachment' import { AriesFrameworkError } from '../../../../error' import { deepEquality, JsonTransformer } from '../../../../utils' -import { DifPresentationExchangeService } from '../../../dif-presentation-exchange' +import { + DifPresentationExchangeService, + DifPresentationExchangeSubmissionLocation, +} from '../../../dif-presentation-exchange' import { W3cCredentialService, ClaimFormat, @@ -185,28 +188,18 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic const { presentation_definition: presentationDefinition, options } = requestAttachment.getDataAsJson() - const credentials: DifPexInputDescriptorToCredentials = proofFormats?.presentationExchange?.credentials ?? {} - if (Object.keys(credentials).length === 0) { - const { areRequirementsSatisfied, requirements } = await ps.getCredentialsForRequest( - agentContext, - presentationDefinition - ) - - if (!areRequirementsSatisfied) { - throw new AriesFrameworkError('Requirements of the presentation definition could not be satisfied') - } - - requirements.forEach((r) => { - r.submissionEntry.forEach((r) => { - credentials[r.inputDescriptorId] = r.verifiableCredentials.map((c) => c.credential) - }) - }) + let credentials: DifPexInputDescriptorToCredentials + if (proofFormats?.presentationExchange?.credentials) { + credentials = proofFormats.presentationExchange.credentials + } else { + const credentialsForRequest = await ps.getCredentialsForRequest(agentContext, presentationDefinition) + credentials = ps.selectCredentialsForRequest(credentialsForRequest) } const presentation = await ps.createPresentation(agentContext, { presentationDefinition, credentialsForInputDescriptor: credentials, - challenge: options?.challenge, + challenge: options?.challenge ?? (await agentContext.wallet.generateNonce()), domain: options?.domain, }) @@ -214,9 +207,12 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic throw new AriesFrameworkError('Invalid amount of verifiable presentations. Only one is allowed.') } + if (presentation.presentationSubmissionLocation === DifPresentationExchangeSubmissionLocation.EXTERNAL) { + throw new AriesFrameworkError('External presentation submission is not supported.') + } + const firstPresentation = presentation.verifiablePresentations[0] - const attachmentData = firstPresentation.encoded as DifPresentationExchangePresentation - const attachment = this.getFormatData(attachmentData, format.attachmentId) + const attachment = this.getFormatData(firstPresentation, format.attachmentId) return { attachment, format } } @@ -235,10 +231,15 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic // TODO: we should probably move this transformation logic into the VC module, so it // can be reused in AFJ when we need to go from encoded -> parsed - if (typeof presentation === 'string') { + if (typeof presentation === 'string' && presentation.includes('~')) { + // NOTE: we need to define in the PEX RFC where to put the presentation_submission + throw new AriesFrameworkError('Received SD-JWT VC in PEX proof format. This is not supported yet.') + } else if (typeof presentation === 'string') { + // If it's a string, we expect it to be a JWT VP parsedPresentation = W3cJwtVerifiablePresentation.fromSerializedJwt(presentation) jsonPresentation = parsedPresentation.presentation.toJSON() } else { + // Otherwise we expect it to be a JSON-LD VP parsedPresentation = JsonTransformer.fromJSON(presentation, W3cJsonLdVerifiablePresentation) jsonPresentation = parsedPresentation.toJSON() } diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts index eaa788fce1..2971de8c00 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts @@ -6,8 +6,7 @@ import type { OpenId4VciCredentialSupported, OpenId4VciIssuerMetadataDisplay, } from '../shared' -import type { AgentContext, ClaimFormat, W3cCredential } from '@aries-framework/core' -import type { SdJwtVcSignOptions } from '@aries-framework/sd-jwt-vc' +import type { AgentContext, ClaimFormat, W3cCredential, SdJwtVcSignOptions } from '@aries-framework/core' export interface OpenId4VciPreAuthorizedCodeFlowConfig { preAuthorizedCode?: string diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierServiceOptions.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierServiceOptions.ts index 7dd4609386..64ea6d387b 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierServiceOptions.ts @@ -3,8 +3,8 @@ import type { DifPresentationExchangeSubmission, VerificationMethod, W3cVerifiablePresentation, + SdJwtVc, } from '@aries-framework/core' -import type { SdJwtVc } from '@aries-framework/sd-jwt-vc' import type { IDTokenPayload, VerifiedOpenID4VPSubmission, diff --git a/packages/openid4vc/src/shared/transform.ts b/packages/openid4vc/src/shared/transform.ts index 5654879721..62e4b6f891 100644 --- a/packages/openid4vc/src/shared/transform.ts +++ b/packages/openid4vc/src/shared/transform.ts @@ -1,5 +1,4 @@ -import type { W3cVerifiableCredential, W3cVerifiablePresentation } from '@aries-framework/core' -import type { SdJwtVc } from '@aries-framework/sd-jwt-vc' +import type { W3cVerifiableCredential, W3cVerifiablePresentation, SdJwtVc } from '@aries-framework/core' import type { W3CVerifiableCredential as SphereonW3cVerifiableCredential, CompactSdJwtVc as SphereonCompactSdJwtVc, From bdf2ca7bc74bfc4c7731a48d4e4e301aad3e1005 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Thu, 25 Jan 2024 17:20:12 +0700 Subject: [PATCH 111/115] a lot of api updates Signed-off-by: Timo Glastra --- packages/anoncreds/src/utils/credential.ts | 2 +- .../tests/InMemoryAnonCredsRegistry.ts | 4 +- .../services/CheqdAnonCredsRegistry.ts | 2 +- packages/core/src/modules/dids/DidsApi.ts | 8 + .../dids/services/DidRegistrarService.ts | 7 + .../dids/services/DidResolverService.ts | 7 + .../DifPresentationExchangeService.ts | 31 +- .../utils/transform.ts | 23 ++ ...fPresentationExchangeProofFormatService.ts | 9 +- .../src/modules/sd-jwt-vc/SdJwtVcService.ts | 12 +- .../sd-jwt-vc/__tests__/sdJwtVc.e2e.test.ts | 2 +- packages/core/src/utils/Hasher.ts | 18 +- packages/core/src/utils/MultiHashEncoder.ts | 3 +- .../ledger/serializeRequestForSignature.ts | 2 +- packages/openid4vc/package.json | 9 +- .../openid4vc-holder/OpenId4VcHolderApi.ts | 85 ++-- .../openid4vc-holder/OpenId4VcHolderModule.ts | 4 +- .../OpenId4VciHolderService.ts | 164 ++++---- .../OpenId4VpHolderService.ts | 247 ----------- .../OpenId4VpHolderServiceOptions.ts | 41 -- .../OpenId4vcSiopHolderService.ts | 294 ++++++++++++++ .../OpenId4vcSiopHolderServiceOptions.ts | 58 +++ .../__tests__/openId4vc-holder-module.test.ts | 4 +- .../__tests__/openid4vci-holder.e2e.test.ts | 13 +- .../__tests__/openid4vp-holder.e2e.test.ts | 28 +- .../openid4vc/src/openid4vc-holder/index.ts | 4 +- .../OpenId4VcIssuerModuleConfig.ts | 28 +- .../OpenId4VcIssuerService.ts | 1 + .../OpenId4VcIssuerServiceOptions.ts | 7 +- .../__tests__/openid4vc-issuer.e2e.test.ts | 55 +-- .../router/accessTokenEndpoint.ts | 6 +- .../router/credentialEndpoint.ts | 4 +- .../src/openid4vc-issuer/router/index.ts | 4 +- .../InMemoryVerifierSessionManager.ts | 323 --------------- .../OpenId4VcSiopVerifierService.ts | 352 ++++++++++++++++ .../OpenId4VcSiopVerifierServiceOptions.ts | 51 +++ .../OpenId4VcVerifierApi.ts | 34 +- .../OpenId4VcVerifierModule.ts | 4 +- .../OpenId4VcVerifierModuleConfig.ts | 40 +- .../OpenId4VcVerifierService.ts | 382 ------------------ .../OpenId4VcVerifierServiceOptions.ts | 91 ----- .../openId4vc-verifier-module.test.ts | 4 +- .../openid4vc/src/openid4vc-verifier/index.ts | 5 +- .../router/authorizationEndpoint.ts | 10 +- .../staticOpConfiguration.ts | 26 -- .../src/shared/models/OpenId4VcJwtIssuer.ts | 13 + packages/openid4vc/src/shared/models/index.ts | 10 + packages/openid4vc/src/shared/transform.ts | 16 + packages/openid4vc/src/shared/utils.ts | 59 +-- packages/openid4vc/tests/utilsVp.ts | 33 +- yarn.lock | 115 ++---- 51 files changed, 1179 insertions(+), 1575 deletions(-) delete mode 100644 packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderService.ts delete mode 100644 packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderServiceOptions.ts create mode 100644 packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts create mode 100644 packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderServiceOptions.ts delete mode 100644 packages/openid4vc/src/openid4vc-verifier/InMemoryVerifierSessionManager.ts create mode 100644 packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts create mode 100644 packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierServiceOptions.ts delete mode 100644 packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierService.ts delete mode 100644 packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierServiceOptions.ts delete mode 100644 packages/openid4vc/src/openid4vc-verifier/staticOpConfiguration.ts create mode 100644 packages/openid4vc/src/shared/models/OpenId4VcJwtIssuer.ts diff --git a/packages/anoncreds/src/utils/credential.ts b/packages/anoncreds/src/utils/credential.ts index 33a7a05c41..8ae3f54382 100644 --- a/packages/anoncreds/src/utils/credential.ts +++ b/packages/anoncreds/src/utils/credential.ts @@ -150,7 +150,7 @@ export function encodeCredentialValue(value: unknown) { value = 'None' } - return new BigNumber(Hasher.hash(Buffer.from(value as string), 'sha2-256')).toString() + return new BigNumber(Hasher.hash(String(value).toString(), 'sha2-256')).toString() } export function assertAttributesMatch(schema: AnonCredsSchema, attributes: CredentialPreviewAttributeOptions[]) { diff --git a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts index da51aa12ec..796245135d 100644 --- a/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts +++ b/packages/anoncreds/tests/InMemoryAnonCredsRegistry.ts @@ -353,9 +353,7 @@ export class InMemoryAnonCredsRegistry implements AnonCredsRegistry { * Does this by hashing the schema id, transforming the hash to a number and taking the first 6 digits. */ function getSeqNoFromSchemaId(schemaId: string) { - const seqNo = Number( - new BigNumber(Hasher.hash(TypedArrayEncoder.fromString(schemaId), 'sha2-256')).toString().slice(0, 5) - ) + const seqNo = Number(new BigNumber(Hasher.hash(schemaId, 'sha2-256')).toString().slice(0, 5)) return seqNo } diff --git a/packages/cheqd/src/anoncreds/services/CheqdAnonCredsRegistry.ts b/packages/cheqd/src/anoncreds/services/CheqdAnonCredsRegistry.ts index 391ce13d92..d6f0049f74 100644 --- a/packages/cheqd/src/anoncreds/services/CheqdAnonCredsRegistry.ts +++ b/packages/cheqd/src/anoncreds/services/CheqdAnonCredsRegistry.ts @@ -142,7 +142,7 @@ export class CheqdAnonCredsRegistry implements AnonCredsRegistry { } const credDefName = `${schema.schema.name}-${credentialDefinition.tag}` - const credDefNameHashBuffer = Hasher.hash(Buffer.from(credDefName), 'sha2-256') + const credDefNameHashBuffer = Hasher.hash(credDefName, 'sha2-256') const credDefResource = { id: utils.uuid(), diff --git a/packages/core/src/modules/dids/DidsApi.ts b/packages/core/src/modules/dids/DidsApi.ts index 4f0cf294bf..d5329299f8 100644 --- a/packages/core/src/modules/dids/DidsApi.ts +++ b/packages/core/src/modules/dids/DidsApi.ts @@ -175,4 +175,12 @@ export class DidsApi { }, }) } + + public get supportedResolverMethods() { + return this.didResolverService.supportedMethods + } + + public get supportedRegistrarMethods() { + return this.didRegistrarService.supportedMethods + } } diff --git a/packages/core/src/modules/dids/services/DidRegistrarService.ts b/packages/core/src/modules/dids/services/DidRegistrarService.ts index cb59457aa0..861110f7a6 100644 --- a/packages/core/src/modules/dids/services/DidRegistrarService.ts +++ b/packages/core/src/modules/dids/services/DidRegistrarService.ts @@ -153,4 +153,11 @@ export class DidRegistrarService { private findRegistrarForMethod(method: string): DidRegistrar | null { return this.didsModuleConfig.registrars.find((r) => r.supportedMethods.includes(method)) ?? null } + + /** + * Get all supported did methods for the did registrar. + */ + public get supportedMethods() { + return Array.from(new Set(this.didsModuleConfig.registrars.flatMap((r) => r.supportedMethods))) + } } diff --git a/packages/core/src/modules/dids/services/DidResolverService.ts b/packages/core/src/modules/dids/services/DidResolverService.ts index 7f97d3f9d1..baf342b89e 100644 --- a/packages/core/src/modules/dids/services/DidResolverService.ts +++ b/packages/core/src/modules/dids/services/DidResolverService.ts @@ -71,4 +71,11 @@ export class DidResolverService { private findResolver(parsed: ParsedDid): DidResolver | null { return this.didsModuleConfig.resolvers.find((r) => r.supportedMethods.includes(parsed.method)) ?? null } + + /** + * Get all supported did methods for the did resolver. + */ + public get supportedMethods() { + return Array.from(new Set(this.didsModuleConfig.resolvers.flatMap((r) => r.supportedMethods))) + } } diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts index afb65b560a..3708a941fc 100644 --- a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts @@ -12,7 +12,6 @@ import type { Query } from '../../storage/StorageService' import type { VerificationMethod } from '../dids' import type { SdJwtVc, SdJwtVcRecord } from '../sd-jwt-vc' import type { W3cVerifiablePresentation, W3cCredentialRecord } from '../vc' -import type { W3cJsonPresentation } from '../vc/models/presentation/W3cJsonPresentation' import type { PresentationSignCallBackParams, SdJwtDecodedVerifiableCredentialWithKbJwtInput, @@ -41,6 +40,7 @@ import { import { DifPresentationExchangeError } from './DifPresentationExchangeError' import { DifPresentationExchangeSubmissionLocation } from './models' import { + getVerifiablePresentationFromEncoded, getSphereonOriginalVerifiablePresentation, getCredentialsForRequest, getPresentationsToCreate, @@ -52,14 +52,7 @@ import { */ @injectable() export class DifPresentationExchangeService { - private pex = new PEX({ - hasher: (data, alg) => { - if (alg !== 'sha-256') { - throw new AriesFrameworkError('Only sha-256 is supported as hashing algorithm.') - } - return Hasher.hash(TypedArrayEncoder.fromString(data), 'sha2-256') - }, - }) + private pex = new PEX({ hasher: Hasher.hash }) public constructor(private w3cCredentialService: W3cCredentialService) {} @@ -221,9 +214,13 @@ export class DifPresentationExchangeService { } return { - verifiablePresentations: verifiablePresentationResultsWithFormat.map( - // Can be JSON or string (sd-jwt / jwt compact) - (r) => r.verifiablePresentationResult.verifiablePresentation as W3cJsonPresentation | string + verifiablePresentations: await Promise.all( + verifiablePresentationResultsWithFormat.map((resultWithFormat) => + getVerifiablePresentationFromEncoded( + agentContext, + resultWithFormat.verifiablePresentationResult.verifiablePresentation + ) + ) ), presentationSubmission, presentationSubmissionLocation: @@ -382,11 +379,14 @@ export class DifPresentationExchangeService { presentationToCreate.subjectIds[0] ) + const w3cPresentation = JsonTransformer.fromJSON(presentationInput, W3cPresentation) + w3cPresentation.holder = verificationMethod.controller + const signedPresentation = await this.w3cCredentialService.signPresentation(agentContext, { format: ClaimFormat.JwtVp, alg: this.getSigningAlgorithmForJwtVc(presentationDefinition, verificationMethod), verificationMethod: verificationMethod.id, - presentation: JsonTransformer.fromJSON(presentationInput, W3cPresentation), + presentation: w3cPresentation, challenge, domain, }) @@ -399,6 +399,9 @@ export class DifPresentationExchangeService { presentationToCreate.subjectIds[0] ) + const w3cPresentation = JsonTransformer.fromJSON(presentationInput, W3cPresentation) + w3cPresentation.holder = verificationMethod.controller + const signedPresentation = await this.w3cCredentialService.signPresentation(agentContext, { format: ClaimFormat.LdpVp, // TODO: we should move the check for which proof to use for a presentation to earlier @@ -407,7 +410,7 @@ export class DifPresentationExchangeService { proofType: this.getProofTypeForLdpVc(agentContext, presentationDefinition, verificationMethod), proofPurpose: 'authentication', verificationMethod: verificationMethod.id, - presentation: JsonTransformer.fromJSON(presentationInput, W3cPresentation), + presentation: w3cPresentation, challenge, domain, }) diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts b/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts index 4feb750477..3c368ab979 100644 --- a/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts +++ b/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts @@ -1,10 +1,16 @@ +import type { AgentContext } from '../../../agent' import type { SdJwtVcRecord, SdJwtVc } from '../../sd-jwt-vc' import type { W3cVerifiablePresentation } from '../../vc' +import type { W3cJsonPresentation } from '../../vc/models/presentation/W3cJsonPresentation' import type { OriginalVerifiableCredential as SphereonOriginalVerifiableCredential, OriginalVerifiablePresentation as SphereonOriginalVerifiablePresentation, + W3CVerifiablePresentation as SphereonW3CVerifiablePresentation, } from '@sphereon/ssi-types' +import { AriesFrameworkError } from '../../../error' +import { JsonTransformer } from '../../../utils' +import { SdJwtVcApi } from '../../sd-jwt-vc' import { W3cCredentialRecord, W3cJsonLdVerifiablePresentation, W3cJwtVerifiablePresentation } from '../../vc' export function getSphereonOriginalVerifiableCredential( @@ -29,3 +35,20 @@ export function getSphereonOriginalVerifiablePresentation( return verifiablePresentation.compact } } + +// TODO: we might want to move this to some generic vc transformation util +export async function getVerifiablePresentationFromEncoded( + agentContext: AgentContext, + encodedVerifiablePresentation: string | W3cJsonPresentation | SphereonW3CVerifiablePresentation +) { + if (typeof encodedVerifiablePresentation === 'string' && encodedVerifiablePresentation.includes('~')) { + const sdJwtVcApi = agentContext.dependencyManager.resolve(SdJwtVcApi) + return sdJwtVcApi.fromCompact(encodedVerifiablePresentation) + } else if (typeof encodedVerifiablePresentation === 'string') { + return W3cJwtVerifiablePresentation.fromSerializedJwt(encodedVerifiablePresentation) + } else if (typeof encodedVerifiablePresentation === 'object' && '@context' in encodedVerifiablePresentation) { + return JsonTransformer.fromJSON(encodedVerifiablePresentation, W3cJsonLdVerifiablePresentation) + } else { + throw new AriesFrameworkError('Unsupported verifiable presentation format') + } +} diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts index fd99ea9944..2445da80ce 100644 --- a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts @@ -212,7 +212,14 @@ export class PresentationExchangeProofFormatService implements ProofFormatServic } const firstPresentation = presentation.verifiablePresentations[0] - const attachment = this.getFormatData(firstPresentation, format.attachmentId) + + // TODO: they should all have `encoded` property so it's easy to use the resulting VP + const encodedFirstPresentation = + firstPresentation instanceof W3cJwtVerifiablePresentation || + firstPresentation instanceof W3cJsonLdVerifiablePresentation + ? firstPresentation.encoded + : firstPresentation?.compact + const attachment = this.getFormatData(encodedFirstPresentation, format.attachmentId) return { attachment, format } } diff --git a/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts b/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts index e0c42785a7..21f05d5e7f 100644 --- a/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts +++ b/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts @@ -142,7 +142,7 @@ export class SdJwtVcService { // FIXME: _sd_hash is missing. See // https://github.com/berendsliedrecht/sd-jwt-ts/issues/8 - _sd_hash: TypedArrayEncoder.toBase64URL(await this.hasher.hasher(compactDerivedSdJwtVc, sdAlg)), + _sd_hash: TypedArrayEncoder.toBase64URL(Hasher.hash(compactDerivedSdJwtVc, sdAlg)), } const compactKbJwt = await new KeyBinding({ header, payload }) @@ -189,7 +189,7 @@ export class SdJwtVcService { const sdJwtParts = compactSdJwtVc.split('~') sdJwtParts.pop() // remove kb-jwt const sdJwtWithoutKbJwt = `${sdJwtParts.join('~')}~` - const sdHash = TypedArrayEncoder.toBase64URL(await this.hasher.hasher(sdJwtWithoutKbJwt, sdAlg)) + const sdHash = TypedArrayEncoder.toBase64URL(Hasher.hash(sdJwtWithoutKbJwt, sdAlg)) // Assert `aud` and `nonce` claims sdJwtVc.keyBinding.assertClaimInPayload('aud', keyBinding.audience) @@ -254,13 +254,7 @@ export class SdJwtVcService { private get hasher(): HasherAndAlgorithm { return { algorithm: HasherAlgorithm.Sha256, - hasher: (input: string, algorithm) => { - if (algorithm !== 'sha-256') { - throw new SdJwtVcError(`Unsupported hashing algorithm used: ${algorithm}. Only sha-256 is supported`) - } - const serializedInput = TypedArrayEncoder.fromString(input) - return Hasher.hash(serializedInput, 'sha2-256') - }, + hasher: Hasher.hash, } } diff --git a/packages/core/src/modules/sd-jwt-vc/__tests__/sdJwtVc.e2e.test.ts b/packages/core/src/modules/sd-jwt-vc/__tests__/sdJwtVc.e2e.test.ts index 50cf4ad2d6..72f55af7ad 100644 --- a/packages/core/src/modules/sd-jwt-vc/__tests__/sdJwtVc.e2e.test.ts +++ b/packages/core/src/modules/sd-jwt-vc/__tests__/sdJwtVc.e2e.test.ts @@ -224,7 +224,7 @@ describe('sd-jwt-vc end to end test', () => { }, }) - const { verification: presentationVerification } = await verifier.modules.sdJwtVc.verify({ + const { verification: presentationVerification } = await verifier.sdJwtVc.verify({ compactSdJwtVc: presentation, keyBinding: { audience: verifierDid, nonce: verifierMetadata.nonce }, requiredClaimKeys: [ diff --git a/packages/core/src/utils/Hasher.ts b/packages/core/src/utils/Hasher.ts index 023a69c708..760a4956fc 100644 --- a/packages/core/src/utils/Hasher.ts +++ b/packages/core/src/utils/Hasher.ts @@ -1,6 +1,9 @@ import { hash as sha256 } from '@stablelib/sha256' -export type HashName = 'sha2-256' +import { TypedArrayEncoder } from './TypedArrayEncoder' + +// TODO: use JWA Hashing Algorithm names +export type HashName = 'sha2-256' | 'sha-256' type HashingMap = { [key in HashName]: (data: Uint8Array) => Uint8Array @@ -8,16 +11,17 @@ type HashingMap = { const hashingMap: HashingMap = { 'sha2-256': (data) => sha256(data), + 'sha-256': (data) => sha256(data), } export class Hasher { - public static hash(data: Uint8Array, hashName: HashName): Uint8Array { - const hashFn = hashingMap[hashName] - - if (!hashFn) { - throw new Error(`Unsupported hash name '${hashName}'`) + public static hash(data: Uint8Array | string, hashName: HashName | string): Uint8Array { + const dataAsUint8Array = typeof data === 'string' ? TypedArrayEncoder.fromString(data) : data + if (hashName in hashingMap) { + const hashFn = hashingMap[hashName as HashName] + return hashFn(dataAsUint8Array) } - return hashFn(data) + throw new Error(`Unsupported hash name '${hashName}'`) } } diff --git a/packages/core/src/utils/MultiHashEncoder.ts b/packages/core/src/utils/MultiHashEncoder.ts index 43a333d495..be4ed42456 100644 --- a/packages/core/src/utils/MultiHashEncoder.ts +++ b/packages/core/src/utils/MultiHashEncoder.ts @@ -14,6 +14,7 @@ type MultiHashCodeMap = { const multiHashNameMap: MultiHashNameMap = { 'sha2-256': 0x12, + 'sha-256': 0x12, } const multiHashCodeMap: MultiHashCodeMap = Object.entries(multiHashNameMap).reduce( @@ -31,7 +32,7 @@ export class MultiHashEncoder { * * @returns a multihash */ - public static encode(data: Uint8Array, hashName: 'sha2-256'): Buffer { + public static encode(data: Uint8Array, hashName: HashName): Buffer { const hash = Hasher.hash(data, hashName) const hashCode = multiHashNameMap[hashName] diff --git a/packages/indy-sdk/src/ledger/serializeRequestForSignature.ts b/packages/indy-sdk/src/ledger/serializeRequestForSignature.ts index 630dcbab2b..b29809aa80 100644 --- a/packages/indy-sdk/src/ledger/serializeRequestForSignature.ts +++ b/packages/indy-sdk/src/ledger/serializeRequestForSignature.ts @@ -46,7 +46,7 @@ function _serializeRequestForSignature(v: any, isTopLevel: boolean, _type?: stri if ((_type == ATTRIB_TYPE || _type == GET_ATTR_TYPE) && (vKey == 'raw' || vKey == 'hash' || vKey == 'enc')) { // do it only for attribute related request if (typeof value !== 'string') throw new Error('Value must be a string for hash') - const hash = Hasher.hash(TypedArrayEncoder.fromString(value), 'sha2-256') + const hash = Hasher.hash(value, 'sha2-256') value = Buffer.from(hash).toString('hex') } diff --git a/packages/openid4vc/package.json b/packages/openid4vc/package.json index 931e7fce18..2ca4ccb45a 100644 --- a/packages/openid4vc/package.json +++ b/packages/openid4vc/package.json @@ -25,12 +25,11 @@ }, "dependencies": { "@aries-framework/core": "0.4.2", - "@aries-framework/openid4vc": "0.4.2", "@sphereon/ssi-types": "^0.18.1", - "@sphereon/oid4vci-client": "0.8.2-next.34", - "@sphereon/oid4vci-common": "0.8.2-next.34", - "@sphereon/oid4vci-issuer": "0.8.2-next.34", - "@sphereon/did-auth-siop": "0.6.0-unstable.0" + "@sphereon/oid4vci-client": "0.8.2-next.46", + "@sphereon/oid4vci-common": "0.8.2-next.46", + "@sphereon/oid4vci-issuer": "0.8.2-next.46", + "@sphereon/did-auth-siop": "0.6.0-unstable.3" }, "devDependencies": { "@types/express": "^4.17.21", diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts index 37f4913d61..257fa5a60d 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts @@ -4,80 +4,52 @@ import type { OpenId4VciAuthCodeFlowOptions, OpenId4VciAcceptCredentialOfferOptions, } from './OpenId4VciHolderServiceOptions' -import type { AuthenticationRequest, PresentationRequest } from './OpenId4VpHolderServiceOptions' -import type { VerificationMethod, DifPexInputDescriptorToCredentials } from '@aries-framework/core' +import type { OpenId4VcSiopAcceptAuthorizationRequestOptions } from './OpenId4vcSiopHolderServiceOptions' import { injectable, AgentContext } from '@aries-framework/core' import { OpenId4VciHolderService } from './OpenId4VciHolderService' -import { OpenId4VpHolderService } from './OpenId4VpHolderService' +import { OpenId4VcSiopHolderService } from './OpenId4vcSiopHolderService' -// FIXME: the holder API is not really consistent with the issuer API -// FIXME: it's not immediately clear which methods are for receiving vc proving /** * @public */ @injectable() export class OpenId4VcHolderApi { - private agentContext: AgentContext - private openId4VciHolderService: OpenId4VciHolderService - private openId4VpHolderService: OpenId4VpHolderService - public constructor( - agentContext: AgentContext, - openId4VcHolderService: OpenId4VciHolderService, - openId4VpHolderService: OpenId4VpHolderService - ) { - this.agentContext = agentContext - this.openId4VciHolderService = openId4VcHolderService - this.openId4VpHolderService = openId4VpHolderService - } + private agentContext: AgentContext, + private openId4VciHolderService: OpenId4VciHolderService, + private openId4VcSiopHolderService: OpenId4VcSiopHolderService + ) {} /** * Resolves the authentication request given as URI or JWT to a unified format, and * verifies the validity of the request. - * The resolved request can be accepted with either @see acceptAuthenticationRequest if it is an - * authentication request or with @see acceptPresentationRequest if it is a proofRequest. * - * @param requestJwtOrUri JWT or an openid:// URI - * @returns the resolved and verified authentication request or presentation request alongside the data required to fulfill the presentation request if possible. - */ - public async resolveProofRequest(requestJwtOrUri: string) { - return await this.openId4VpHolderService.resolveProofRequest(this.agentContext, requestJwtOrUri) - } - - /** - * Accepts the authentication request after it has been resolved and verified with @see resolveProofRequest. + * The resolved request can be accepted with the @see acceptSiopAuthorizationRequest. + * + * If the authorization request uses OpenID4VP and included presentation definitions, + * a `presentationExchange` property will be defined with credentials that satisfy the + * incoming request. When `presentationExchange` is present, you MUST supply `presentationExchange` + * when calling `acceptSiopAuthorizationRequest` as well. * - * @param authenticationRequest - The verified authorization request object. - * @param verificationMethod - The method used for creating the authentication proof. - * @returns @see ProofSubmissionResponse containing the status of the submission. + * @param requestJwtOrUri JWT or an SIOPv2 request URI + * @returns the resolved and verified authentication request. */ - public async acceptAuthenticationRequest( - authenticationRequest: AuthenticationRequest, - verificationMethod: VerificationMethod - ) { - return await this.openId4VpHolderService.acceptAuthenticationRequest( - this.agentContext, - verificationMethod, - authenticationRequest - ) + public async resolveSiopAuthorizationRequest(requestJwtOrUri: string) { + return this.openId4VcSiopHolderService.resolveAuthorizationRequest(this.agentContext, requestJwtOrUri) } /** - * Accepts the proof request with a presentation after it has been resolved and verified @see resolveProofRequest. + * Accepts the authentication request after it has been resolved and verified with {@link resolveSiopAuthorizationRequest}. + * + * If the resolved authorization request included a `presentationExchange` property, you MUST supply `presentationExchange` + * in the `options` parameter. * - * @param presentationRequest - The verified authorization request object containing the presentation definition. - * @param presentation.submission - The presentation submission object obtained from @see resolveProofRequest - * @param presentation.submissionEntryIndexes - The indexes of the credentials in the presentation submission that should be send to the verifier. - * @returns @see ProofSubmissionResponse containing the status of the submission. + * If no `presentationExchange` property is present, you MUST supply `openIdTokenIssuer` in the `options` parameter. */ - public async acceptPresentationRequest( - // FIXME: more unique interface names: OpenId4VpPresentationRequest - presentationRequest: PresentationRequest, - credentials: DifPexInputDescriptorToCredentials - ) { - return await this.openId4VpHolderService.acceptProofRequest(this.agentContext, presentationRequest, credentials) + public async acceptSiopAuthorizationRequest(options: OpenId4VcSiopAcceptAuthorizationRequestOptions) { + return await this.openId4VcSiopHolderService.acceptAuthorizationRequest(this.agentContext, options) } /** @@ -92,7 +64,10 @@ export class OpenId4VcHolderApi { } /** - * This function is to be used with the Authorization Code Flow. + * This function is to be used to receive an credential in OpenID4VCI using the Authorization Code Flow. + * + * Not to be confused with the {@link resolveSiopAuthorizationRequest}, which is only used for SIOP requests. + * * It will generate the authorization request URI based on the provided options. * The authorization request URI is used to obtain the authorization code. Currently this needs to be done manually. * @@ -104,7 +79,7 @@ export class OpenId4VcHolderApi { * @param authCodeFlowOptions * @returns The authorization request URI alongside the code verifier and original @param authCodeFlowOptions */ - public async resolveAuthorizationRequest( + public async resolveIssuanceAuthorizationRequest( resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer, authCodeFlowOptions: OpenId4VciAuthCodeFlowOptions ) { @@ -119,7 +94,6 @@ export class OpenId4VcHolderApi { * Accepts a credential offer using the pre-authorized code flow. * @param resolvedCredentialOffer Obtained through @see resolveCredentialOffer * @param acceptCredentialOfferOptions - * @returns ( @see W3cCredentialRecord | @see SdJwtRecord )[] */ public async acceptCredentialOfferUsingPreAuthorizedCode( resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer, @@ -134,10 +108,9 @@ export class OpenId4VcHolderApi { /** * Accepts a credential offer using the authorization code flow. * @param resolvedCredentialOffer Obtained through @see resolveCredentialOffer - * @param resolvedAuthorizationRequest Obtained through @see resolveAuthorizationRequest + * @param resolvedAuthorizationRequest Obtained through @see resolveIssuanceAuthorizationRequest * @param code The authorization code obtained via the authorization request URI * @param acceptCredentialOfferOptions - * @returns ( @see W3cCredentialRecord | @see SdJwtRecord )[] */ public async acceptCredentialOfferUsingAuthorizationCode( resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer, diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderModule.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderModule.ts index b610e2ffe5..a804058def 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderModule.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderModule.ts @@ -4,7 +4,7 @@ import { AgentConfig } from '@aries-framework/core' import { OpenId4VcHolderApi } from './OpenId4VcHolderApi' import { OpenId4VciHolderService } from './OpenId4VciHolderService' -import { OpenId4VpHolderService } from './OpenId4VpHolderService' +import { OpenId4VcSiopHolderService } from './OpenId4vcSiopHolderService' /** * @public @module OpenId4VcHolderModule @@ -29,6 +29,6 @@ export class OpenId4VcHolderModule implements Module { // Services dependencyManager.registerSingleton(OpenId4VciHolderService) - dependencyManager.registerSingleton(OpenId4VpHolderService) + dependencyManager.registerSingleton(OpenId4VcSiopHolderService) } } diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts index d800b626ac..ff5e5b559c 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts @@ -74,84 +74,6 @@ import { openId4VciSupportedCredentialFormats, } from './OpenId4VciHolderServiceOptions' -// FIXME: this is also defined in the sphereon lib, is there a reason we don't use that one? -// We use this to get PAR working and because we don't use the oid4vci client in sphereon's lib -async function createAuthorizationRequestUri(options: { - credentialOffer: OpenId4VciCredentialOfferPayload - metadata: OpenId4VciResolvedCredentialOffer['metadata'] - clientId: string - codeChallenge: string - codeChallengeMethod: CodeChallengeMethod - authDetails?: AuthorizationDetails | AuthorizationDetails[] - redirectUri: string - scope?: string[] -}) { - const { scope, authDetails, metadata, clientId, codeChallenge, codeChallengeMethod, redirectUri } = options - let nonEmptyScope = !scope || scope.length === 0 ? undefined : scope - const nonEmptyAuthDetails = !authDetails || authDetails.length === 0 ? undefined : authDetails - - // Scope and authorization_details can be used in the same authorization request - // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param - if (!nonEmptyScope && !nonEmptyAuthDetails) { - throw new AriesFrameworkError(`Please provide a 'scope' or 'authDetails' via the options.`) - } - - // Authorization servers supporting PAR SHOULD include the URL of their pushed authorization request endpoint in their authorization server metadata document - // Note that the presence of pushed_authorization_request_endpoint is sufficient for a client to determine that it may use the PAR flow. - const parEndpoint = metadata.credentialIssuerMetadata.pushed_authorization_request_endpoint - - const authorizationEndpoint = metadata.credentialIssuerMetadata?.authorization_endpoint - - if (!authorizationEndpoint && !parEndpoint) { - throw new AriesFrameworkError( - "Server metadata does not contain an 'authorization_endpoint' which is required for the 'Authorization Code Flow'" - ) - } - - // add 'openid' scope if not present - if (nonEmptyScope && !nonEmptyScope?.includes('openid')) { - nonEmptyScope = ['openid', ...nonEmptyScope] - } - - const queryObj: Record = { - client_id: clientId, - response_type: ResponseType.AUTH_CODE, - code_challenge_method: codeChallengeMethod, - code_challenge: codeChallenge, - redirect_uri: redirectUri, - } - - if (nonEmptyScope) queryObj['scope'] = nonEmptyScope.join(' ') - - if (nonEmptyAuthDetails) - queryObj['authorization_details'] = JSON.stringify(handleAuthorizationDetails(nonEmptyAuthDetails, metadata)) - - const issuerState = options.credentialOffer.grants?.authorization_code?.issuer_state - if (issuerState) queryObj['issuer_state'] = issuerState - - if (parEndpoint) { - const body = new URLSearchParams(queryObj) - const response = await formPost(parEndpoint, body) - if (!response.successBody) { - throw new AriesFrameworkError(`Could not acquire the authorization request uri from '${parEndpoint}'`) - } - return convertJsonToURI( - { request_uri: response.successBody.request_uri, client_id: clientId, response_type: ResponseType.AUTH_CODE }, - { - baseUrl: authorizationEndpoint, - uriTypeProperties: ['request_uri', 'client_id', 'response_type'], - mode: JsonURIMode.X_FORM_WWW_URLENCODED, - } - ) - } else { - return convertJsonToURI(queryObj, { - baseUrl: authorizationEndpoint, - uriTypeProperties: ['redirect_uri', 'scope', 'authorization_details', 'issuer_state'], - mode: JsonURIMode.X_FORM_WWW_URLENCODED, - }) - } -} - @injectable() export class OpenId4VciHolderService { private logger: Logger @@ -242,9 +164,6 @@ export class OpenId4VciHolderService { } } - // FIXME: this is an oid4vci authorization request - // while we also support siop/oid4vp authorization requests - // need to make sure difference is clear public async resolveAuthorizationRequest( agentContext: AgentContext, resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer, @@ -252,7 +171,7 @@ export class OpenId4VciHolderService { ): Promise { const { credentialOfferPayload, metadata, offeredCredentials } = resolvedCredentialOffer const codeVerifier = `${await agentContext.wallet.generateNonce()}${await agentContext.wallet.generateNonce()}` - const codeVerifierSha256 = Hasher.hash(TypedArrayEncoder.fromString(codeVerifier), 'sha2-256') + const codeVerifierSha256 = Hasher.hash(codeVerifier, 'sha2-256') const codeChallenge = TypedArrayEncoder.toBase64URL(codeVerifierSha256) this.logger.debug('Converted code_verifier to code_challenge', { @@ -717,3 +636,84 @@ export class OpenId4VciHolderService { } } } + +// NOTE: this is also defined in the sphereon lib, but we use +// this custom method to get PAR working and because we don't +// use the oid4vci client in sphereon's lib +// Once PAR is supported in the sphereon lib, we should to try remove this +// and use the one from the sphereon lib +async function createAuthorizationRequestUri(options: { + credentialOffer: OpenId4VciCredentialOfferPayload + metadata: OpenId4VciResolvedCredentialOffer['metadata'] + clientId: string + codeChallenge: string + codeChallengeMethod: CodeChallengeMethod + authDetails?: AuthorizationDetails | AuthorizationDetails[] + redirectUri: string + scope?: string[] +}) { + const { scope, authDetails, metadata, clientId, codeChallenge, codeChallengeMethod, redirectUri } = options + let nonEmptyScope = !scope || scope.length === 0 ? undefined : scope + const nonEmptyAuthDetails = !authDetails || authDetails.length === 0 ? undefined : authDetails + + // Scope and authorization_details can be used in the same authorization request + // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-rar-23#name-relationship-to-scope-param + if (!nonEmptyScope && !nonEmptyAuthDetails) { + throw new AriesFrameworkError(`Please provide a 'scope' or 'authDetails' via the options.`) + } + + // Authorization servers supporting PAR SHOULD include the URL of their pushed authorization request endpoint in their authorization server metadata document + // Note that the presence of pushed_authorization_request_endpoint is sufficient for a client to determine that it may use the PAR flow. + const parEndpoint = metadata.credentialIssuerMetadata.pushed_authorization_request_endpoint + + const authorizationEndpoint = metadata.credentialIssuerMetadata?.authorization_endpoint + + if (!authorizationEndpoint && !parEndpoint) { + throw new AriesFrameworkError( + "Server metadata does not contain an 'authorization_endpoint' which is required for the 'Authorization Code Flow'" + ) + } + + // add 'openid' scope if not present + if (nonEmptyScope && !nonEmptyScope?.includes('openid')) { + nonEmptyScope = ['openid', ...nonEmptyScope] + } + + const queryObj: Record = { + client_id: clientId, + response_type: ResponseType.AUTH_CODE, + code_challenge_method: codeChallengeMethod, + code_challenge: codeChallenge, + redirect_uri: redirectUri, + } + + if (nonEmptyScope) queryObj['scope'] = nonEmptyScope.join(' ') + + if (nonEmptyAuthDetails) + queryObj['authorization_details'] = JSON.stringify(handleAuthorizationDetails(nonEmptyAuthDetails, metadata)) + + const issuerState = options.credentialOffer.grants?.authorization_code?.issuer_state + if (issuerState) queryObj['issuer_state'] = issuerState + + if (parEndpoint) { + const body = new URLSearchParams(queryObj) + const response = await formPost(parEndpoint, body) + if (!response.successBody) { + throw new AriesFrameworkError(`Could not acquire the authorization request uri from '${parEndpoint}'`) + } + return convertJsonToURI( + { request_uri: response.successBody.request_uri, client_id: clientId, response_type: ResponseType.AUTH_CODE }, + { + baseUrl: authorizationEndpoint, + uriTypeProperties: ['request_uri', 'client_id', 'response_type'], + mode: JsonURIMode.X_FORM_WWW_URLENCODED, + } + ) + } else { + return convertJsonToURI(queryObj, { + baseUrl: authorizationEndpoint, + uriTypeProperties: ['redirect_uri', 'scope', 'authorization_details', 'issuer_state'], + mode: JsonURIMode.X_FORM_WWW_URLENCODED, + }) + } +} diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderService.ts deleted file mode 100644 index 1f924c9f8e..0000000000 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderService.ts +++ /dev/null @@ -1,247 +0,0 @@ -import type { - AgentContext, - DifPexInputDescriptorToCredentials, - VerificationMethod, - W3cVerifiablePresentation, -} from '@aries-framework/core' -import type { W3CVerifiablePresentation } from '@sphereon/ssi-types' - -import { - AriesFrameworkError, - DidsApi, - injectable, - W3cJsonLdVerifiablePresentation, - asArray, - parseDid, - DifPresentationExchangeService, -} from '@aries-framework/core' -import { - CheckLinkedDomain, - OP, - ResponseIss, - ResponseMode, - SupportedVersion, - VPTokenLocation, - VerificationMode, -} from '@sphereon/did-auth-siop' - -import { getResolver, getSuppliedSignatureFromVerificationMethod, getSupportedDidMethods } from '../shared/utils' - -import { - isVerifiedAuthorizationRequestWithPresentationDefinition, - type AuthenticationRequest, - type PresentationRequest, - type ProofSubmissionResponse, - type ResolvedProofRequest, -} from './OpenId4VpHolderServiceOptions' - -@injectable() -export class OpenId4VpHolderService { - public constructor(private presentationExchangeService: DifPresentationExchangeService) {} - - private async getOpenIdProvider( - agentContext: AgentContext, - options: { - verificationMethod?: VerificationMethod - } - ) { - const { verificationMethod } = options - - const builder = OP.builder() - .withExpiresIn(6000) - .withIssuer(ResponseIss.SELF_ISSUED_V2) - .withResponseMode(ResponseMode.POST) - .withSupportedVersions([SupportedVersion.SIOPv2_D11, SupportedVersion.SIOPv2_D12_OID4VP_D18]) - .withCustomResolver(getResolver(agentContext)) - .withCheckLinkedDomain(CheckLinkedDomain.NEVER) - - if (verificationMethod) { - const { signature, did, kid, alg } = await getSuppliedSignatureFromVerificationMethod( - agentContext, - verificationMethod - ) - - builder.withSuppliedSignature(signature, did, kid, alg) - } - - // Add did methods - const supportedDidMethods = getSupportedDidMethods(agentContext) - for (const supportedDidMethod of supportedDidMethods) { - builder.addDidMethod(supportedDidMethod) - } - - const openidProvider = builder.build() - - return openidProvider - } - - public async resolveProofRequest(agentContext: AgentContext, requestJwtOrUri: string): Promise { - const openidProvider = await this.getOpenIdProvider(agentContext, {}) - - // parsing happens automatically in verifyAuthorizationRequest - const verifiedAuthorizationRequest = await openidProvider.verifyAuthorizationRequest(requestJwtOrUri, { - verification: { - mode: VerificationMode.INTERNAL, - resolveOpts: { resolver: getResolver(agentContext), noUniversalResolverFallback: true }, - }, - }) - - agentContext.config.logger.debug( - `verified SIOP Authorization Request for issuer '${verifiedAuthorizationRequest.issuer}'` - ) - agentContext.config.logger.debug(`requestJwtOrUri '${requestJwtOrUri}'`) - - // If the presentationDefinitions array property is present it means the op.verifyAuthorizationRequest - // already has established that the Presentation Definition(s) itself were valid and present. - // It has populated the presentationDefinitions array for you. - // If the definition was not valid, the verify method would have thrown an error, - // which means you should never continue the authentication flow! - const presentationDefs = verifiedAuthorizationRequest.presentationDefinitions - if (!presentationDefs || presentationDefs.length === 0) { - return { proofType: 'authentication', authenticationRequest: verifiedAuthorizationRequest } - } - - // FIXME: I don't see any reason why we would support multiple presentation definitions - // but the library does support it. For now we only support a single presentation definition. - if (!isVerifiedAuthorizationRequestWithPresentationDefinition(verifiedAuthorizationRequest)) { - throw new AriesFrameworkError( - 'Only SIOPv2 authorization request including a single presentation definition are supported.' - ) - } - - const credentialsForRequest = await this.presentationExchangeService.getCredentialsForRequest( - agentContext, - verifiedAuthorizationRequest.presentationDefinitions[0].definition - ) - - return { proofType: 'presentation', presentationRequest: verifiedAuthorizationRequest, credentialsForRequest } - } - - /** - * Send a SIOPv2 authentication response to the relying party including a verifiable - * presentation based on OpenID4VP. - */ - public async acceptAuthenticationRequest( - agentContext: AgentContext, - verificationMethod: VerificationMethod, - authenticationRequest: AuthenticationRequest - ): Promise { - const openidProvider = await this.getOpenIdProvider(agentContext, { verificationMethod }) - - // TODO: jwk support - const subjectSyntaxTypesSupported = authenticationRequest.registrationMetadataPayload.subject_syntax_types_supported - if (subjectSyntaxTypesSupported) { - const { method } = parseDid(verificationMethod.id) - if (subjectSyntaxTypesSupported.includes(`did:${method}`) === false) { - throw new AriesFrameworkError( - [ - 'The provided verification method is not supported by the issuer.', - `Supported subject syntax types: '${subjectSyntaxTypesSupported.join(', ')}'`, - ].join('\n') - ) - } - } - - const authorizationResponseWithCorrelationId = await openidProvider.createAuthorizationResponse( - authenticationRequest, - { - signature: await getSuppliedSignatureFromVerificationMethod(agentContext, verificationMethod), - issuer: verificationMethod.controller, - verification: { - resolveOpts: { resolver: getResolver(agentContext), noUniversalResolverFallback: true }, - mode: VerificationMode.INTERNAL, - }, - // https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-aud-of-a-request-object - audience: authenticationRequest.authorizationRequestPayload.client_id, - } - ) - - const response = await openidProvider.submitAuthorizationResponse(authorizationResponseWithCorrelationId) - return { - ok: response.status === 200, - status: response.status, - submittedResponse: authorizationResponseWithCorrelationId.response.payload, - } - } - - /** - * Send a SIOPv2 authentication response to the relying party including a verifiable - * presentation based on OpenID4VP. - */ - public async acceptProofRequest( - agentContext: AgentContext, - presentationRequest: PresentationRequest, - credentialsForInputDescriptor: DifPexInputDescriptorToCredentials - ): Promise { - // FIXME: make sure nonce and clientId are also verified in the verify proof method - const nonce = await presentationRequest.authorizationRequest.getMergedProperty('nonce') - if (!nonce) { - throw new AriesFrameworkError("Unable to extract 'nonce' from authorization request") - } - - const clientId = await presentationRequest.authorizationRequest.getMergedProperty('client_id') - if (!clientId) { - throw new AriesFrameworkError("Unable to extract 'client_id' from authorization request") - } - - const { verifiablePresentations, presentationSubmission } = - await this.presentationExchangeService.createPresentation(agentContext, { - credentialsForInputDescriptor, - presentationDefinition: presentationRequest.presentationDefinitions[0].definition, - challenge: nonce, - domain: clientId, - }) - - const verificationMethod = await this.getVerificationMethodFromVerifiablePresentation( - agentContext, - verifiablePresentations[0] - ) - - const openidProvider = await this.getOpenIdProvider(agentContext, { verificationMethod }) - - const authorizationResponseWithCorrelationId = await openidProvider.createAuthorizationResponse( - presentationRequest, - { - signature: await getSuppliedSignatureFromVerificationMethod(agentContext, verificationMethod), - issuer: verificationMethod.controller, - // https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-aud-of-a-request-object - audience: presentationRequest.authorizationRequestPayload.client_id, - - presentationExchange: { - verifiablePresentations: verifiablePresentations.map((vp) => vp.encoded as W3CVerifiablePresentation), - presentationSubmission, - vpTokenLocation: VPTokenLocation.AUTHORIZATION_RESPONSE, - }, - } - ) - - const response = await openidProvider.submitAuthorizationResponse(authorizationResponseWithCorrelationId) - return { - ok: response.status >= 200 && response.status < 300, - status: response.status, - submittedResponse: authorizationResponseWithCorrelationId.response.payload, - } - } - - private async getVerificationMethodFromVerifiablePresentation( - agentContext: AgentContext, - verifiablePresentation: W3cVerifiablePresentation - ) { - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - - let verificationMethodId: string - if (verifiablePresentation instanceof W3cJsonLdVerifiablePresentation) { - const [firstProof] = asArray(verifiablePresentation.proof) - if (!firstProof) throw new AriesFrameworkError('Verifiable presentation does not contain a proof') - - verificationMethodId = firstProof.verificationMethod - } else { - const kid = verifiablePresentation.jwt.header.kid - if (!kid) throw new AriesFrameworkError('Verifiable Presentation does not contain a kid in the jwt header') - verificationMethodId = kid - } - - const didDocument = await didsApi.resolveDidDocument(verificationMethodId) - return didDocument.dereferenceKey(verificationMethodId, ['authentication']) - } -} diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderServiceOptions.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderServiceOptions.ts deleted file mode 100644 index 175dc38772..0000000000 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VpHolderServiceOptions.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { DifPexCredentialsForRequest } from '@aries-framework/core/src' -import type { - AuthorizationResponsePayload, - PresentationDefinitionWithLocation, - VerifiedAuthorizationRequest, -} from '@sphereon/did-auth-siop' - -export type AuthenticationRequest = VerifiedAuthorizationRequest - -export type PresentationRequest = VerifiedAuthorizationRequest & { - presentationDefinitions: [PresentationDefinitionWithLocation] -} - -export function isVerifiedAuthorizationRequestWithPresentationDefinition( - request: VerifiedAuthorizationRequest -): request is PresentationRequest { - return ( - request.presentationDefinitions !== undefined && - request.presentationDefinitions.length === 1 && - request.presentationDefinitions?.[0]?.definition !== undefined - ) -} - -export type ResolvedPresentationRequest = { - proofType: 'presentation' - presentationRequest: PresentationRequest - credentialsForRequest: DifPexCredentialsForRequest -} - -export type ResolvedAuthenticationRequest = { - proofType: 'authentication' - authenticationRequest: AuthenticationRequest -} - -export type ResolvedProofRequest = ResolvedAuthenticationRequest | ResolvedPresentationRequest - -export type ProofSubmissionResponse = { - ok: boolean - status: number - submittedResponse: AuthorizationResponsePayload -} diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts new file mode 100644 index 0000000000..29d18f31f6 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts @@ -0,0 +1,294 @@ +import type { + OpenId4VcSiopAcceptAuthorizationRequestOptions, + OpenId4VcSiopResolvedAuthorizationRequest, +} from './OpenId4vcSiopHolderServiceOptions' +import type { OpenId4VcJwtIssuer } from '../shared' +import type { AgentContext, SdJwtVc, W3cVerifiablePresentation } from '@aries-framework/core' +import type { VerifiedAuthorizationRequest, PresentationExchangeResponseOpts } from '@sphereon/did-auth-siop' + +import { + W3cJwtVerifiablePresentation, + parseDid, + AriesFrameworkError, + DidsApi, + injectable, + W3cJsonLdVerifiablePresentation, + asArray, + DifPresentationExchangeService, +} from '@aries-framework/core' +import { + CheckLinkedDomain, + OP, + ResponseIss, + ResponseMode, + SupportedVersion, + VPTokenLocation, + VerificationMode, +} from '@sphereon/did-auth-siop' + +import { getSphereonVerifiablePresentation } from '../shared/transform' +import { getSphereonDidResolver, getSphereonSuppliedSignatureFromJwtIssuer } from '../shared/utils' + +@injectable() +export class OpenId4VcSiopHolderService { + public constructor(private presentationExchangeService: DifPresentationExchangeService) {} + + public async resolveAuthorizationRequest( + agentContext: AgentContext, + requestJwtOrUri: string + ): Promise { + const openidProvider = await this.getOpenIdProvider(agentContext, {}) + + // parsing happens automatically in verifyAuthorizationRequest + const verifiedAuthorizationRequest = await openidProvider.verifyAuthorizationRequest(requestJwtOrUri, { + verification: { + // FIXME: we want custom verification, but not supported currently + // https://github.com/Sphereon-Opensource/SIOP-OID4VP/issues/55 + mode: VerificationMode.INTERNAL, + resolveOpts: { resolver: getSphereonDidResolver(agentContext), noUniversalResolverFallback: true }, + }, + }) + + agentContext.config.logger.debug( + `verified SIOP Authorization Request for issuer '${verifiedAuthorizationRequest.issuer}'` + ) + agentContext.config.logger.debug(`requestJwtOrUri '${requestJwtOrUri}'`) + + if ( + verifiedAuthorizationRequest.presentationDefinitions && + verifiedAuthorizationRequest.presentationDefinitions.length > 1 + ) { + throw new AriesFrameworkError('Only a single presentation definition is supported.') + } + + const presentationDefinition = verifiedAuthorizationRequest.presentationDefinitions?.[0]?.definition + + return { + authorizationRequest: verifiedAuthorizationRequest, + + // Parameters related to DIF Presentation Exchange + presentationExchange: presentationDefinition + ? { + definition: presentationDefinition, + credentialsForRequest: await this.presentationExchangeService.getCredentialsForRequest( + agentContext, + presentationDefinition + ), + } + : undefined, + } + } + + public async acceptAuthorizationRequest( + agentContext: AgentContext, + options: OpenId4VcSiopAcceptAuthorizationRequestOptions + ) { + const { authorizationRequest, presentationExchange } = options + let openIdTokenIssuer = options.openIdTokenIssuer + let presentationExchangeOptions: PresentationExchangeResponseOpts | undefined = undefined + + // Handle presentation exchange part + if (authorizationRequest.presentationDefinitions) { + if (!presentationExchange) { + throw new AriesFrameworkError( + 'Authorization request included presentation definition. `presentationExchange` MUST be supplied to accept authorization requests.' + ) + } + + // FIXME: make sure nonce and clientId are also verified in the verify proof method + const nonce = await authorizationRequest.authorizationRequest.getMergedProperty('nonce') + if (!nonce) { + throw new AriesFrameworkError("Unable to extract 'nonce' from authorization request") + } + + const clientId = await authorizationRequest.authorizationRequest.getMergedProperty('client_id') + if (!clientId) { + throw new AriesFrameworkError("Unable to extract 'client_id' from authorization request") + } + + const { verifiablePresentations, presentationSubmission } = + await this.presentationExchangeService.createPresentation(agentContext, { + credentialsForInputDescriptor: presentationExchange.credentials, + presentationDefinition: authorizationRequest.presentationDefinitions[0].definition, + challenge: nonce, + domain: clientId, + }) + + presentationExchangeOptions = { + verifiablePresentations: verifiablePresentations.map((vp) => getSphereonVerifiablePresentation(vp)), + presentationSubmission, + vpTokenLocation: VPTokenLocation.AUTHORIZATION_RESPONSE, + } + + if (!openIdTokenIssuer) { + openIdTokenIssuer = this.getOpenIdTokenIssuerFromVerifiablePresentation(verifiablePresentations[0]) + } + } else if (options.presentationExchange) { + throw new AriesFrameworkError( + '`presentationExchange` was supplied, but no presentation definition was found in the presentaiton request.' + ) + } + + if (!openIdTokenIssuer) { + throw new AriesFrameworkError( + 'Unable to create authorization response. openIdTokenIssuer MUST be supplied when no presentation is active.' + ) + } + + this.assertValidTokenIssuer(authorizationRequest, openIdTokenIssuer) + const openidProvider = await this.getOpenIdProvider(agentContext, { + openIdTokenIssuer, + }) + + const suppliedSignature = await getSphereonSuppliedSignatureFromJwtIssuer(agentContext, openIdTokenIssuer) + const authorizationResponseWithCorrelationId = await openidProvider.createAuthorizationResponse( + authorizationRequest, + { + signature: suppliedSignature, + issuer: suppliedSignature.did, + verification: { + resolveOpts: { resolver: getSphereonDidResolver(agentContext), noUniversalResolverFallback: true }, + mode: VerificationMode.INTERNAL, + }, + presentationExchange: presentationExchangeOptions, + // https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-aud-of-a-request-object + audience: authorizationRequest.authorizationRequestPayload.client_id, + } + ) + + const response = await openidProvider.submitAuthorizationResponse(authorizationResponseWithCorrelationId) + return { + ok: response.status === 200, + status: response.status, + submittedResponse: authorizationResponseWithCorrelationId.response.payload, + } + } + + private async getOpenIdProvider( + agentContext: AgentContext, + options: { + openIdTokenIssuer?: OpenId4VcJwtIssuer + } = {} + ) { + const { openIdTokenIssuer } = options + + const builder = OP.builder() + .withExpiresIn(6000) + .withIssuer(ResponseIss.SELF_ISSUED_V2) + .withResponseMode(ResponseMode.POST) + .withSupportedVersions([SupportedVersion.SIOPv2_D11, SupportedVersion.SIOPv2_D12_OID4VP_D18]) + .withCustomResolver(getSphereonDidResolver(agentContext)) + .withCheckLinkedDomain(CheckLinkedDomain.NEVER) + + if (openIdTokenIssuer) { + const suppliedSignature = await getSphereonSuppliedSignatureFromJwtIssuer(agentContext, openIdTokenIssuer) + builder.withSignature(suppliedSignature) + } + + // Add did methods + const supportedDidMethods = agentContext.dependencyManager.resolve(DidsApi).supportedResolverMethods + for (const supportedDidMethod of supportedDidMethods) { + builder.addDidMethod(supportedDidMethod) + } + + const openidProvider = builder.build() + + return openidProvider + } + + private getOpenIdTokenIssuerFromVerifiablePresentation( + verifiablePresentation: W3cVerifiablePresentation | SdJwtVc + ): OpenId4VcJwtIssuer { + let openIdTokenIssuer: OpenId4VcJwtIssuer + + if (verifiablePresentation instanceof W3cJsonLdVerifiablePresentation) { + const [firstProof] = asArray(verifiablePresentation.proof) + if (!firstProof) throw new AriesFrameworkError('Verifiable presentation does not contain a proof') + + if (!firstProof.verificationMethod.startsWith('did:')) { + throw new AriesFrameworkError( + 'Verifiable presentation proof verificationMethod is not a did. Unable to extract openIdTokenIssuer from verifiable presentation' + ) + } + + openIdTokenIssuer = { + method: 'did', + didUrl: firstProof.verificationMethod, + } + } else if (verifiablePresentation instanceof W3cJwtVerifiablePresentation) { + const kid = verifiablePresentation.jwt.header.kid + + if (!kid) throw new AriesFrameworkError('Verifiable Presentation does not contain a kid in the jwt header') + if (kid.startsWith('#') && verifiablePresentation.presentation.holderId) { + openIdTokenIssuer = { + didUrl: `${verifiablePresentation.presentation.holderId}${kid}`, + method: 'did', + } + } else if (kid.startsWith('did:')) { + openIdTokenIssuer = { + didUrl: kid, + method: 'did', + } + } else { + throw new AriesFrameworkError( + "JWT W3C Verifiable presentation does not include did in JWT header 'kid'. Unable to extract openIdTokenIssuer from verifiable presentation" + ) + } + } else { + const cnf = verifiablePresentation.payload.cnf + // FIXME: SD-JWT VC should have better payload typing, so this doesn't become so ugly + if ( + !cnf || + typeof cnf !== 'object' || + !('kid' in cnf) || + typeof cnf.kid !== 'string' || + !cnf.kid.startsWith('did:') || + !cnf.kid.includes('#') + ) { + throw new AriesFrameworkError( + "SD-JWT Verifiable presentation has no 'cnf' claim or does not include 'cnf' claim where 'kid' is a didUrl pointing to a key. Unable to extract openIdTokenIssuer from verifiable presentation" + ) + } + + openIdTokenIssuer = { + didUrl: cnf.kid, + method: 'did', + } + } + + return openIdTokenIssuer + } + + private assertValidTokenIssuer( + authorizationRequest: VerifiedAuthorizationRequest, + openIdTokenIssuer: OpenId4VcJwtIssuer + ) { + // TODO: jwk thumbprint support + const subjectSyntaxTypesSupported = authorizationRequest.registrationMetadataPayload.subject_syntax_types_supported + if (!subjectSyntaxTypesSupported) { + throw new AriesFrameworkError( + 'subject_syntax_types_supported is not supplied in the registration metadata. subject_syntax_types is REQUIRED.' + ) + } + + let allowedSubjectSyntaxTypes: string[] = [] + if (openIdTokenIssuer.method === 'did') { + const parsedDid = parseDid(openIdTokenIssuer.didUrl) + + // Either did: or did (for all did methods) is allowed + allowedSubjectSyntaxTypes = [`did:${parsedDid.method}`, 'did'] + } else { + throw new AriesFrameworkError("Only 'did' is supported as openIdTokenIssuer at the moment") + } + + // At least one of the allowed subject syntax types must be supported by the RP + if (!allowedSubjectSyntaxTypes.some((allowed) => subjectSyntaxTypesSupported.includes(allowed))) { + throw new AriesFrameworkError( + [ + 'The provided openIdTokenIssuer is not supported by the relying party.', + `Supported subject syntax types: '${subjectSyntaxTypesSupported.join(', ')}'`, + ].join('\n') + ) + } + } +} diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderServiceOptions.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderServiceOptions.ts new file mode 100644 index 0000000000..7f901793f1 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderServiceOptions.ts @@ -0,0 +1,58 @@ +import type { + OpenId4VcJwtIssuer, + OpenId4VcSiopVerifiedAuthorizationRequest, + OpenId4VcSiopAuthorizationResponsePayload, +} from '../shared' +import type { + DifPexCredentialsForRequest, + DifPexInputDescriptorToCredentials, + DifPresentationExchangeDefinition, +} from '@aries-framework/core' + +export interface OpenId4VcSiopResolvedAuthorizationRequest { + /** + * Parameters related to DIF Presentation Exchange. Only defined when + * the request included + */ + presentationExchange?: { + definition: DifPresentationExchangeDefinition + credentialsForRequest: DifPexCredentialsForRequest + } + + /** + * The verified authorization request. + */ + authorizationRequest: OpenId4VcSiopVerifiedAuthorizationRequest +} + +export interface OpenId4VcSiopAcceptAuthorizationRequestOptions { + /** + * Parameters related to DIF Presentation Exchange. MUST be present when the resolved + * authorization request included a `presentationExchange` parameter. + */ + presentationExchange?: { + credentials: DifPexInputDescriptorToCredentials + } + + /** + * The issuer of the ID Token. + * + * REQUIRED when presentation exchange is not used. + * + * In case presentation exchange is used, and `openIdTokenIssuer` is not provided, the issuer of the ID Token + * will be extracted from the signer of the first verifiable presentation. + */ + openIdTokenIssuer?: OpenId4VcJwtIssuer + + /** + * The verified authorization request. + */ + authorizationRequest: OpenId4VcSiopVerifiedAuthorizationRequest +} + +// FIXME: rethink properties +export interface OpenId4VcSiopAuthorizationResponseSubmission { + ok: boolean + status: number + submittedResponse: OpenId4VcSiopAuthorizationResponsePayload +} diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/openId4vc-holder-module.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/openId4vc-holder-module.test.ts index 4de88f6039..79092044f1 100644 --- a/packages/openid4vc/src/openid4vc-holder/__tests__/openId4vc-holder-module.test.ts +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/openId4vc-holder-module.test.ts @@ -3,7 +3,7 @@ import type { DependencyManager } from '@aries-framework/core' import { OpenId4VcHolderApi } from '../OpenId4VcHolderApi' import { OpenId4VcHolderModule } from '../OpenId4VcHolderModule' import { OpenId4VciHolderService } from '../OpenId4VciHolderService' -import { OpenId4VpHolderService } from '../OpenId4VpHolderService' +import { OpenId4VcSiopHolderService } from '../OpenId4vcSiopHolderService' const dependencyManager = { registerInstance: jest.fn(), @@ -22,6 +22,6 @@ describe('OpenId4VcHolderModule', () => { expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(2) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VciHolderService) - expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VpHolderService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VcSiopHolderService) }) }) diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts index 633d0a03ab..a98fa20082 100644 --- a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts @@ -266,11 +266,14 @@ describe('OpenId4VcHolder', () => { const resolved = await holder.modules.openId4VcHolder.resolveCredentialOffer(fixture.credentialOffer) - const resolvedAuthorizationRequest = await holder.modules.openId4VcHolder.resolveAuthorizationRequest(resolved, { - clientId: 'test-client', - redirectUri: 'http://example.com', - scope: ['openid', 'UniversityDegree'], - }) + const resolvedAuthorizationRequest = await holder.modules.openId4VcHolder.resolveIssuanceAuthorizationRequest( + resolved, + { + clientId: 'test-client', + redirectUri: 'http://example.com', + scope: ['openid', 'UniversityDegree'], + } + ) await expect( holder.modules.openId4VcHolder.acceptCredentialOfferUsingAuthorizationCode( diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts index 6030da45cc..6c7b4d4166 100644 --- a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts @@ -1,5 +1,5 @@ import type { AgentType } from '../../../tests/utils' -import type { OpenId4VcCreateAuthorizationRequestOptions } from '../../openid4vc-verifier' +import type { OpenId4VcSiopCreateAuthorizationRequestOptions } from '../../openid4vc-verifier' import type { Express } from 'express' import type { Server } from 'http' @@ -81,7 +81,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { }) it('siop request with static metadata', async () => { - const createProofRequestOptions: OpenId4VcCreateAuthorizationRequestOptions = { + const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { verificationMethod: verifier.verificationMethod, openIdProvider: staticOpOpenIdConfigEdDSA, } @@ -98,7 +98,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { if (result.proofType == 'presentation') throw new Error('Expected an authenticationRequest') //////////////////////////// OP (accept the verified request) //////////////////////////// - const { submittedResponse, status } = await holder.agent.modules.openId4VcHolder.acceptAuthenticationRequest( + const { submittedResponse, status } = await holder.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest( result.authenticationRequest, holder.verificationMethod ) @@ -141,7 +141,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { .get('/.well-known/openid-configuration') .reply(200, staticOpOpenIdConfigEdDSA) - const createProofRequestOptions: OpenId4VcCreateAuthorizationRequestOptions = { + const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { verificationMethod: verifier.verificationMethod, // TODO: if provided this way client metadata is not resolved for the verification method openIdProvider: 'https://helloworld.com', @@ -159,7 +159,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { if (result.proofType == 'presentation') throw new Error('Expected a proofType') //////////////////////////// OP (accept the verified request) //////////////////////////// - const { submittedResponse, status } = await holder.agent.modules.openId4VcHolder.acceptAuthenticationRequest( + const { submittedResponse, status } = await holder.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest( result.authenticationRequest, holder.verificationMethod ) @@ -185,7 +185,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { }) it('resolving vp request with no credentials', async () => { - const createProofRequestOptions: OpenId4VcCreateAuthorizationRequestOptions = { + const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { verificationMethod: verifier.verificationMethod, openIdProvider: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, @@ -208,7 +208,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), }) - const createProofRequestOptions: OpenId4VcCreateAuthorizationRequestOptions = { + const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { verificationMethod: verifier.verificationMethod, openIdProvider: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, @@ -235,7 +235,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), }) - const createProofRequestOptions: OpenId4VcCreateAuthorizationRequestOptions = { + const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { verificationMethod: verifier.verificationMethod, openIdProvider: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, @@ -273,7 +273,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), }) - const createProofRequestOptions: OpenId4VcCreateAuthorizationRequestOptions = { + const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { verificationMethod: verifier.verificationMethod, openIdProvider: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, @@ -307,7 +307,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), }) - const createProofRequestOptions: OpenId4VcCreateAuthorizationRequestOptions = { + const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { verificationMethod: verifier.verificationMethod, openIdProvider: staticOpOpenIdConfigEdDSA, presentationDefinition: combinePresentationDefinitions([ @@ -392,7 +392,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), }) - const createProofRequestOptions: OpenId4VcCreateAuthorizationRequestOptions = { + const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { verificationMethod: verifier.verificationMethod, openIdProvider: staticOpOpenIdConfigEdDSA, presentationDefinition: combinePresentationDefinitions([ @@ -423,7 +423,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), }) - const createProofRequestOptions: OpenId4VcCreateAuthorizationRequestOptions = { + const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { verificationMethod: verifier.verificationMethod, openIdProvider: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgePresentationDefinition, @@ -491,7 +491,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { credential, }) - const createProofRequestOptions: OpenId4VcCreateAuthorizationRequestOptions = { + const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { verificationMethod: verifier.verificationMethod, openIdProvider: staticOpOpenIdConfigEdDSA, presentationDefinition: openBadgeCredentialPresentationDefinitionLdpVc, @@ -560,7 +560,7 @@ describe('OpenId4VcHolder | OpenID4VP', () => { credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), }) - const createProofRequestOptions: OpenId4VcCreateAuthorizationRequestOptions = { + const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { verificationMethod: verifier.verificationMethod, openIdProvider: staticOpOpenIdConfigEdDSA, presentationDefinition: combinePresentationDefinitions([ diff --git a/packages/openid4vc/src/openid4vc-holder/index.ts b/packages/openid4vc/src/openid4vc-holder/index.ts index ab5529210a..2b7a8d1d5b 100644 --- a/packages/openid4vc/src/openid4vc-holder/index.ts +++ b/packages/openid4vc/src/openid4vc-holder/index.ts @@ -2,5 +2,5 @@ export * from './OpenId4VcHolderApi' export * from './OpenId4VcHolderModule' export * from './OpenId4VciHolderService' export * from './OpenId4VciHolderServiceOptions' -export * from './OpenId4VpHolderService' -export * from './OpenId4VpHolderServiceOptions' +export * from './OpenId4vcSiopHolderService' +export * from './OpenId4vcSiopHolderServiceOptions' diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts index c2ec8fc0dd..d4efe593b4 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerModuleConfig.ts @@ -1,14 +1,12 @@ -import type { AccessTokenEndpointConfig, CredentialEndpointConfig } from './router' +import type { OpenId4VciAccessTokenEndpointConfig, OpenId4VciCredentialEndpointConfig } from './router' import type { AgentContext, Optional } from '@aries-framework/core' -import type { CNonceState, CredentialOfferSession, IStateManager, StateType, URIState } from '@sphereon/oid4vci-common' +import type { CNonceState, CredentialOfferSession, IStateManager, URIState } from '@sphereon/oid4vci-common' import type { Router } from 'express' import { MemoryStates } from '@sphereon/oid4vci-issuer' import { importExpress } from '../shared/router' -export type StateManagerFactory = () => IStateManager - const DEFAULT_C_NONCE_EXPIRES_IN = 5 * 60 * 1000 // 5 minutes const DEFAULT_TOKEN_EXPIRES_IN = 3 * 60 * 1000 // 3 minutes const DEFAULT_PRE_AUTH_CODE_EXPIRES_IN = 3 * 60 * 1000 // 3 minutes @@ -30,17 +28,12 @@ export interface OpenId4VcIssuerModuleConfigOptions { router?: Router endpoints: { - credential: Optional + credential: Optional accessToken?: Optional< - AccessTokenEndpointConfig, + OpenId4VciAccessTokenEndpointConfig, 'cNonceExpiresInSeconds' | 'endpointPath' | 'preAuthorizedCodeExpirationInSeconds' | 'tokenExpiresInSeconds' > } - - // FIXME: remove - cNonceStateManagerFactory?: StateManagerFactory - credentialOfferSessionManagerFactory?: StateManagerFactory - uriStateManagerFactory?: StateManagerFactory } export class OpenId4VcIssuerModuleConfig { @@ -67,7 +60,7 @@ export class OpenId4VcIssuerModuleConfig { /** * Get the credential endpoint config, with default values set */ - public get credentialEndpoint(): CredentialEndpointConfig { + public get credentialEndpoint(): OpenId4VciCredentialEndpointConfig { // Use user supplied options, or return defaults. const userOptions = this.options.endpoints.credential @@ -80,7 +73,7 @@ export class OpenId4VcIssuerModuleConfig { /** * Get the access token endpoint config, with default values set */ - public get accessTokenEndpoint(): AccessTokenEndpointConfig { + public get accessTokenEndpoint(): OpenId4VciAccessTokenEndpointConfig { // Use user supplied options, or return defaults. const userOptions = this.options.endpoints.accessToken ?? {} @@ -94,29 +87,32 @@ export class OpenId4VcIssuerModuleConfig { } } + // FIXME: rework (no in-memory) public getUriStateManager(agentContext: AgentContext) { const value = this.uriStateManagerMap.get(agentContext.contextCorrelationId) if (value) return value - const newValue = this.options.uriStateManagerFactory?.() ?? new MemoryStates() + const newValue = new MemoryStates() this.uriStateManagerMap.set(agentContext.contextCorrelationId, newValue) return newValue } + // FIXME: rework (no in-memory) public getCredentialOfferSessionStateManager(agentContext: AgentContext) { const value = this.credentialOfferSessionManagerMap.get(agentContext.contextCorrelationId) if (value) return value - const newValue = this.options.credentialOfferSessionManagerFactory?.() ?? new MemoryStates() + const newValue = new MemoryStates() this.credentialOfferSessionManagerMap.set(agentContext.contextCorrelationId, newValue) return newValue } + // FIXME: rework (no in-memory) public getCNonceStateManager(agentContext: AgentContext) { const value = this.cNonceStateManagerMap.get(agentContext.contextCorrelationId) if (value) return value - const newValue = this.options.cNonceStateManagerFactory?.() ?? new MemoryStates() + const newValue = new MemoryStates() this.cNonceStateManagerMap.set(agentContext.contextCorrelationId, newValue) return newValue } diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts index f1fbf9b51a..8e630a0e7d 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts @@ -112,6 +112,7 @@ export class OpenId4VcIssuerService { const { uri, session } = await vcIssuer.createCredentialOfferURI({ grants: await this.getGrantsFromConfig(agentContext, preAuthorizedCodeFlowConfig, authorizationCodeFlowConfig), credentials: offeredCredentials, + // TODO: support hosting of credential offers within AFJ credentialOfferUri: options.hostedCredentialOfferUrl, baseUri: options.baseUri, }) diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts index 2971de8c00..f7c5d9cf33 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts @@ -67,9 +67,10 @@ export interface OpenId4VciCreateCredentialResponseOptions { } // FIXME: Flows: -// - provide credential data at time of offer creation -// - provide credential data at time of calling createCredentialResponse -// - provide credential data dynamically using this method +// - provide credential data at time of offer creation (NOT SUPPORTED) +// - provide credential data at time of calling createCredentialResponse (partially supported by passing in mapper to this method -> preferred as it gives you request data dynamically) +// - provide credential data dynamically using this method (SUPPORTED) +// mapper should get input data passed (which is supplied to offer or create response) like credentialDataSupplierInput in sphereon lib export type OpenId4VciCredentialRequestToCredentialMapper = (options: { agentContext: AgentContext diff --git a/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.e2e.test.ts b/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.e2e.test.ts index 467333efe8..a2767b3b3b 100644 --- a/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.e2e.test.ts @@ -68,11 +68,6 @@ const universityDegreeCredentialSdJwt = { vct: 'UniversityDegreeCredential', } satisfies OpenId4VciCredentialSupportedWithId -const baseCredentialRequestOptions = { - scheme: 'openid-credential-offer', - baseUri: 'openid4vc-issuer.com', -} - const modules = { openId4VcIssuer: new OpenId4VcIssuerModule({ baseUrl: 'https://openid4vc-issuer.com', @@ -288,8 +283,6 @@ describe('OpenId4VcIssuer', () => { preAuthorizedCode, userPinRequired: false, }, - // FIXME: can we take the base uri from the config? Do we want to provide this? - ...baseCredentialRequestOptions, }) expect(result.credentialOfferPayload).toEqual({ @@ -305,7 +298,7 @@ describe('OpenId4VcIssuer', () => { }) expect(result.credentialOfferUri).toEqual( - `openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FUniversityDegreeCredentialSdJwt%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` + `openid-credential-offer://?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FUniversityDegreeCredentialSdJwt%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` ) const issuerMetadata = await issuer.modules.openId4VcIssuer.getIssuerMetadata(openId4VcIssuer.issuerId) @@ -321,6 +314,7 @@ describe('OpenId4VcIssuer', () => { credentialRequest, credentialRequestToCredentialMapper: () => ({ + format: 'vc+sd-jwt', payload: { vct: 'UniversityDegreeCredential', university: 'innsbruck', degree: 'bachelor' }, issuer: { method: 'did', didUrl: issuerVerificationMethod.id }, holder: { method: 'did', didUrl: holderVerificationMethod.id }, @@ -357,17 +351,17 @@ describe('OpenId4VcIssuer', () => { preAuthorizedCode, userPinRequired: false, }, - ...baseCredentialRequestOptions, }) expect(result.credentialOfferUri).toEqual( - `openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` + `openid-credential-offer://?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` ) const issuerMetadata = await issuer.modules.openId4VcIssuer.getIssuerMetadata(openId4VcIssuer.issuerId) const issueCredentialResponse = await issuer.modules.openId4VcIssuer.createCredentialResponse({ issuerId: openId4VcIssuer.issuerId, credentialRequestToCredentialMapper: () => ({ + format: 'jwt_vc', credential: new W3cCredential({ type: openBadgeCredential.types, issuer: new W3cIssuer({ id: issuerDid }), @@ -414,7 +408,6 @@ describe('OpenId4VcIssuer', () => { preAuthorizedCode, userPinRequired: false, }, - ...baseCredentialRequestOptions, }) ).rejects.toThrowError( "Offered credential 'invalid id' is not part of credentials_supported of the issuer metadata." @@ -437,11 +430,10 @@ describe('OpenId4VcIssuer', () => { preAuthorizedCode, userPinRequired: false, }, - ...baseCredentialRequestOptions, }) expect(result.credentialOfferUri).toEqual( - `openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` + `openid-credential-offer://?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` ) const issuerMetadata = await issuer.modules.openId4VcIssuer.getIssuerMetadata(openId4VcIssuer.issuerId) @@ -478,11 +470,10 @@ describe('OpenId4VcIssuer', () => { preAuthorizedCode, userPinRequired: false, }, - ...baseCredentialRequestOptions, }) expect(result.credentialOfferUri).toEqual( - `openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%2C%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FUniversityDegreeCredentialLd%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` + `openid-credential-offer://?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%2C%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FUniversityDegreeCredentialLd%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` ) const issuerMetadata = await issuer.modules.openId4VcIssuer.getIssuerMetadata(openId4VcIssuer.issuerId) @@ -495,6 +486,7 @@ describe('OpenId4VcIssuer', () => { nonce: cNonce, }), credentialRequestToCredentialMapper: () => ({ + format: 'jwt_vc', credential: new W3cCredential({ type: universityDegreeCredentialLd.types, issuer: new W3cIssuer({ id: issuerDid }), @@ -534,11 +526,10 @@ describe('OpenId4VcIssuer', () => { preAuthorizedCode, userPinRequired: false, }, - ...baseCredentialRequestOptions, }) expect(result.credentialOfferUri).toEqual( - `openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` + `openid-credential-offer://?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` ) const issuerMetadata = await issuer.modules.openId4VcIssuer.getIssuerMetadata(openId4VcIssuer.issuerId) @@ -577,11 +568,10 @@ describe('OpenId4VcIssuer', () => { authorizationCodeFlowConfig: { issuerState, }, - ...baseCredentialRequestOptions, }) expect(result.credentialOfferUri).toEqual( - `openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%221234567890%22%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` + `openid-credential-offer://?credential_offer=%7B%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%221234567890%22%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` ) const issuerMetadata = await issuer.modules.openId4VcIssuer.getIssuerMetadata(openId4VcIssuer.issuerId) @@ -595,6 +585,7 @@ describe('OpenId4VcIssuer', () => { clientId: 'required', }), credentialRequestToCredentialMapper: () => ({ + format: 'jwt_vc', credential: new W3cCredential({ type: ['VerifiableCredential', 'OpenBadgeCredential'], issuer: new W3cIssuer({ id: issuerDid }), @@ -621,47 +612,41 @@ describe('OpenId4VcIssuer', () => { it('create credential offer and retrieve it from the uri (pre authorized flow)', async () => { const preAuthorizedCode = '1234567890' - const hostedCredentialOfferUri = 'https://openid4vc-issuer.com/credential-offer-uri' + const hostedCredentialOfferUrl = 'https://openid4vc-issuer.com/credential-offer-uri' const { credentialOfferUri, credentialOfferPayload } = await issuer.modules.openId4VcIssuer.createCredentialOffer({ - ...baseCredentialRequestOptions, issuerId: openId4VcIssuer.issuerId, offeredCredentials: [openBadgeCredential.id], - credentialOfferUri: hostedCredentialOfferUri, + hostedCredentialOfferUrl, preAuthorizedCodeFlowConfig: { preAuthorizedCode, userPinRequired: false, }, }) - expect(credentialOfferUri).toEqual( - `openid-credential-offer://openid4vc-issuer.com?credential_offer_uri=${hostedCredentialOfferUri}` - ) + expect(credentialOfferUri).toEqual(`openid-credential-offer://?credential_offer_uri=${hostedCredentialOfferUrl}`) const credentialOfferReceivedByUri = await issuer.modules.openId4VcIssuer.getCredentialOfferFromUri( - hostedCredentialOfferUri + hostedCredentialOfferUrl ) expect(credentialOfferPayload).toEqual(credentialOfferReceivedByUri) }) it('create credential offer and retrieve it from the uri (authorizationCodeFlow)', async () => { - const hostedCredentialOfferUri = 'https://openid4vc-issuer.com/credential-offer-uri' + const hostedCredentialOfferUrl = 'https://openid4vc-issuer.com/credential-offer-uri' const { credentialOfferUri, credentialOfferPayload } = await issuer.modules.openId4VcIssuer.createCredentialOffer({ offeredCredentials: [openBadgeCredential.id], issuerId: openId4VcIssuer.issuerId, - ...baseCredentialRequestOptions, - credentialOfferUri: hostedCredentialOfferUri, + hostedCredentialOfferUrl, authorizationCodeFlowConfig: { issuerState: '1234567890' }, }) - expect(credentialOfferUri).toEqual( - `openid-credential-offer://openid4vc-issuer.com?credential_offer_uri=${hostedCredentialOfferUri}` - ) + expect(credentialOfferUri).toEqual(`openid-credential-offer://?credential_offer_uri=${hostedCredentialOfferUrl}`) const credentialOfferReceivedByUri = await issuer.modules.openId4VcIssuer.getCredentialOfferFromUri( - hostedCredentialOfferUri + hostedCredentialOfferUrl ) expect(credentialOfferPayload).toEqual(credentialOfferReceivedByUri) @@ -683,16 +668,16 @@ describe('OpenId4VcIssuer', () => { preAuthorizedCode, userPinRequired: false, }, - ...baseCredentialRequestOptions, }) expect(result.credentialOfferUri).toEqual( - `openid-credential-offer://openid4vc-issuer.com?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%2C%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FUniversityDegreeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` + `openid-credential-offer://?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%2C%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FUniversityDegreeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` ) const credentialRequestToCredentialMapper: OpenId4VciCredentialRequestToCredentialMapper = ({ credentialsSupported, }) => ({ + format: 'jwt_vc', credential: new W3cCredential({ type: credentialsSupported[0].id === openBadgeCredential.id diff --git a/packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts b/packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts index 6afdcaa40e..0fcefe2deb 100644 --- a/packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-issuer/router/accessTokenEndpoint.ts @@ -23,7 +23,7 @@ import { getRequestContext, sendErrorResponse } from '../../shared/router' import { OpenId4VcIssuerModuleConfig } from '../OpenId4VcIssuerModuleConfig' import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' -export interface AccessTokenEndpointConfig { +export interface OpenId4VciAccessTokenEndpointConfig { /** * The path at which the token endpoint should be made available. Note that it will be * hosted at a subpath to take into account multiple tenants and issuers. @@ -54,7 +54,7 @@ export interface AccessTokenEndpointConfig { tokenExpiresInSeconds: number } -export function configureAccessTokenEndpoint(router: Router, config: AccessTokenEndpointConfig) { +export function configureAccessTokenEndpoint(router: Router, config: OpenId4VciAccessTokenEndpointConfig) { router.post( config.endpointPath, verifyTokenRequest({ preAuthorizedCodeExpirationInSeconds: config.preAuthorizedCodeExpirationInSeconds }), @@ -89,7 +89,7 @@ function getJwtSignerCallback(agentContext: AgentContext, signerPublicKey: Key): } } -export function handleTokenRequest(config: AccessTokenEndpointConfig) { +export function handleTokenRequest(config: OpenId4VciAccessTokenEndpointConfig) { const { tokenExpiresInSeconds, cNonceExpiresInSeconds } = config return async (request: OpenId4VcIssuanceRequest, response: Response, next: NextFunction) => { diff --git a/packages/openid4vc/src/openid4vc-issuer/router/credentialEndpoint.ts b/packages/openid4vc/src/openid4vc-issuer/router/credentialEndpoint.ts index 23f79ba25a..5986be4b1c 100644 --- a/packages/openid4vc/src/openid4vc-issuer/router/credentialEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-issuer/router/credentialEndpoint.ts @@ -6,7 +6,7 @@ import type { Router, Response } from 'express' import { getRequestContext, sendErrorResponse } from '../../shared/router' import { OpenId4VcIssuerService } from '../OpenId4VcIssuerService' -export interface CredentialEndpointConfig { +export interface OpenId4VciCredentialEndpointConfig { /** * The path at which the credential endpoint should be made available. Note that it will be * hosted at a subpath to take into account multiple tenants and issuers. @@ -21,7 +21,7 @@ export interface CredentialEndpointConfig { credentialRequestToCredentialMapper: OpenId4VciCredentialRequestToCredentialMapper } -export function configureCredentialEndpoint(router: Router, config: CredentialEndpointConfig) { +export function configureCredentialEndpoint(router: Router, config: OpenId4VciCredentialEndpointConfig) { router.post(config.endpointPath, async (request: OpenId4VcIssuanceRequest, response: Response, next) => { const { agentContext, issuer } = getRequestContext(request) diff --git a/packages/openid4vc/src/openid4vc-issuer/router/index.ts b/packages/openid4vc/src/openid4vc-issuer/router/index.ts index 4d8bae100e..fc1bb807ee 100644 --- a/packages/openid4vc/src/openid4vc-issuer/router/index.ts +++ b/packages/openid4vc/src/openid4vc-issuer/router/index.ts @@ -1,4 +1,4 @@ -export { configureAccessTokenEndpoint, AccessTokenEndpointConfig } from './accessTokenEndpoint' -export { configureCredentialEndpoint, CredentialEndpointConfig } from './credentialEndpoint' +export { configureAccessTokenEndpoint, OpenId4VciAccessTokenEndpointConfig } from './accessTokenEndpoint' +export { configureCredentialEndpoint, OpenId4VciCredentialEndpointConfig } from './credentialEndpoint' export { configureIssuerMetadataEndpoint } from './metadataEndpoint' export { OpenId4VcIssuanceRequest } from './requestContext' diff --git a/packages/openid4vc/src/openid4vc-verifier/InMemoryVerifierSessionManager.ts b/packages/openid4vc/src/openid4vc-verifier/InMemoryVerifierSessionManager.ts deleted file mode 100644 index c880c2d755..0000000000 --- a/packages/openid4vc/src/openid4vc-verifier/InMemoryVerifierSessionManager.ts +++ /dev/null @@ -1,323 +0,0 @@ -import type { VerifyProofResponseOptions } from './OpenId4VcVerifierServiceOptions' -import type { Logger } from '@aries-framework/core' -import type { - AuthorizationEvent, - AuthorizationRequest, - AuthorizationRequestState, - AuthorizationResponse, - AuthorizationResponseState, - IRPSessionManager as SphereonRPSessionManager, -} from '@sphereon/did-auth-siop' -import type { PresentationDefinitionV1, PresentationDefinitionV2 } from '@sphereon/pex-models' -import type { EventEmitter } from 'events' - -import { AriesFrameworkError } from '@aries-framework/core' -import { - AuthorizationEvents, - AuthorizationRequestStateStatus, - AuthorizationResponseStateStatus, -} from '@sphereon/did-auth-siop' - -export type PresentationDefinitionForCorrelationId = - | { - proofType: 'presentation' - presentationDefinition: PresentationDefinitionV1 | PresentationDefinitionV2 - } - | { - proofType: 'authentication' - } - -export interface IInMemoryVerifierSessionManager extends SphereonRPSessionManager { - getVerifyProofResponseOptions(correlationId: string): Promise - saveVerifyProofResponseOptions( - correlationId: string, - presentationDefinitionForCorrelationId: VerifyProofResponseOptions - ): Promise -} - -/** - * Please note that this session manager is not really meant to be used in large production settings, as it stores everything in memory! - * It also doesn't do scheduled cleanups. It runs a cleanup whenever a request or response is received. In a high-volume production setting you will want scheduled cleanups running in the background - * Since this is a low level library we have not created a full-fledged implementation. - * We suggest to create your own implementation using the event system of the library - */ -export class InMemoryVerifierSessionManager implements IInMemoryVerifierSessionManager { - private readonly authorizationRequests: Record = {} - private readonly authorizationResponses: Record = {} - private readonly logger: Logger - - private readonly nonceToCorrelationId: Record = {} - - private readonly stateToCorrelationId: Record = {} - - private readonly correlationIdToVerifyProofResponseOptions: Record = {} - - private readonly maxAgeInSeconds: number - - private static getKeysForCorrelationId(mapping: Record, correlationId: string): number[] { - return Object.entries(mapping) - .filter((entry) => entry[1] === correlationId) - .map((filtered) => Number.parseInt(filtered[0])) - } - - public constructor(ee: EventEmitter, logger: Logger, opts?: { maxAgeInSeconds?: number }) { - this.logger = logger - this.maxAgeInSeconds = opts?.maxAgeInSeconds ?? 5 * 60 - ee.on(AuthorizationEvents.ON_AUTH_REQUEST_CREATED_SUCCESS, this.onAuthorizationRequestCreatedSuccess.bind(this)) - ee.on(AuthorizationEvents.ON_AUTH_REQUEST_CREATED_FAILED, this.onAuthorizationRequestCreatedFailed.bind(this)) - ee.on(AuthorizationEvents.ON_AUTH_REQUEST_SENT_SUCCESS, this.onAuthorizationRequestSentSuccess.bind(this)) - ee.on(AuthorizationEvents.ON_AUTH_REQUEST_SENT_FAILED, this.onAuthorizationRequestSentFailed.bind(this)) - ee.on(AuthorizationEvents.ON_AUTH_RESPONSE_RECEIVED_SUCCESS, this.onAuthorizationResponseReceivedSuccess.bind(this)) - ee.on(AuthorizationEvents.ON_AUTH_RESPONSE_RECEIVED_FAILED, this.onAuthorizationResponseReceivedFailed.bind(this)) - ee.on(AuthorizationEvents.ON_AUTH_RESPONSE_VERIFIED_SUCCESS, this.onAuthorizationResponseVerifiedSuccess.bind(this)) - ee.on(AuthorizationEvents.ON_AUTH_RESPONSE_VERIFIED_FAILED, this.onAuthorizationResponseVerifiedFailed.bind(this)) - } - public async getVerifyProofResponseOptions(correlationId: string): Promise { - return this.correlationIdToVerifyProofResponseOptions[correlationId] - } - - public async saveVerifyProofResponseOptions( - correlationId: string, - verifyProofResponseOptions: VerifyProofResponseOptions - ) { - await this.cleanup() - this.correlationIdToVerifyProofResponseOptions[correlationId] = verifyProofResponseOptions - } - - public async getRequestStateByCorrelationId( - correlationId: string, - errorOnNotFound?: boolean - ): Promise { - return await this.getFromMapping('correlationId', correlationId, this.authorizationRequests, errorOnNotFound) - } - - public async getRequestStateByNonce( - nonce: string, - errorOnNotFound?: boolean - ): Promise { - return await this.getFromMapping('nonce', nonce, this.authorizationRequests, errorOnNotFound) - } - - public async getRequestStateByState( - state: string, - errorOnNotFound?: boolean - ): Promise { - return await this.getFromMapping('state', state, this.authorizationRequests, errorOnNotFound) - } - - public async getResponseStateByCorrelationId( - correlationId: string, - errorOnNotFound?: boolean - ): Promise { - return await this.getFromMapping('correlationId', correlationId, this.authorizationResponses, errorOnNotFound) - } - - public async getResponseStateByNonce( - nonce: string, - errorOnNotFound?: boolean - ): Promise { - return await this.getFromMapping('nonce', nonce, this.authorizationResponses, errorOnNotFound) - } - - public async getResponseStateByState( - state: string, - errorOnNotFound?: boolean - ): Promise { - return await this.getFromMapping('state', state, this.authorizationResponses, errorOnNotFound) - } - - private async getFromMapping( - type: 'nonce' | 'state' | 'correlationId', - value: string, - mapping: Record, - errorOnNotFound?: boolean - ): Promise { - const correlationId = - type === 'correlationId' ? value : await this.getCorrelationIdImpl(type, value, errorOnNotFound) - if (!correlationId) throw new AriesFrameworkError(`Could not find ${type} from correlation id ${correlationId}`) - - const result = mapping[correlationId] - if (!result && errorOnNotFound) - throw new AriesFrameworkError(`Could not find ${type} from correlation id ${correlationId}`) - return result - } - - private async onAuthorizationRequestCreatedSuccess(event: AuthorizationEvent): Promise { - this.updateState('request', event, AuthorizationRequestStateStatus.CREATED).catch((error) => - this.logger.error(JSON.stringify(error)) - ) - } - - private async onAuthorizationRequestCreatedFailed(event: AuthorizationEvent): Promise { - this.updateState('request', event, AuthorizationRequestStateStatus.ERROR).catch((error) => - this.logger.error(JSON.stringify(error)) - ) - } - - private async onAuthorizationRequestSentSuccess(event: AuthorizationEvent): Promise { - this.updateState('request', event, AuthorizationRequestStateStatus.SENT).catch((error) => - this.logger.error(JSON.stringify(error)) - ) - } - - private async onAuthorizationRequestSentFailed(event: AuthorizationEvent): Promise { - this.updateState('request', event, AuthorizationRequestStateStatus.ERROR).catch((error) => - this.logger.error(JSON.stringify(error)) - ) - } - - private async onAuthorizationResponseReceivedSuccess( - event: AuthorizationEvent - ): Promise { - await this.updateState('response', event, AuthorizationResponseStateStatus.RECEIVED) - } - - private async onAuthorizationResponseReceivedFailed(event: AuthorizationEvent): Promise { - await this.updateState('response', event, AuthorizationResponseStateStatus.ERROR) - } - - private async onAuthorizationResponseVerifiedFailed(event: AuthorizationEvent): Promise { - await this.updateState('response', event, AuthorizationResponseStateStatus.ERROR) - } - - private async onAuthorizationResponseVerifiedSuccess( - event: AuthorizationEvent - ): Promise { - await this.updateState('response', event, AuthorizationResponseStateStatus.VERIFIED) - } - - public async getCorrelationIdByNonce(nonce: string, errorOnNotFound?: boolean): Promise { - return await this.getCorrelationIdImpl('nonce', nonce, errorOnNotFound) - } - - public async getCorrelationIdByState(state: string, errorOnNotFound?: boolean): Promise { - return await this.getCorrelationIdImpl('state', state, errorOnNotFound) - } - - private async getCorrelationIdImpl( - type: 'nonce' | 'state', - key: string, - errorOnNotFound?: boolean - ): Promise { - let correlationId: string - if (type === 'nonce') { - correlationId = this.nonceToCorrelationId[key] - } else if (type === 'state') { - correlationId = this.stateToCorrelationId[key] - } else { - throw new AriesFrameworkError(`Unknown type ${type}`) - } - - if (!correlationId && errorOnNotFound) throw new AriesFrameworkError(`Could not find ${type} '${key}'`) - - return correlationId - } - - private async updateMapping( - mapping: Record, - event: AuthorizationEvent, - propertyKey: string, - value: T, - allowExisting: boolean - ) { - const key = (await event.subject.getMergedProperty(propertyKey)) as string - if (!key) { - throw new AriesFrameworkError(`No value found for key ${value} in Authorization Request`) - } - - const existing = mapping[key] - - if (existing) { - if (!allowExisting) { - throw new AriesFrameworkError(`Mapping exists for key ${propertyKey} and we do not allow overwriting values`) - } else if (existing !== value) { - throw new AriesFrameworkError('Value changed for key') - } - } - if (!value) { - delete mapping[key] - } else { - mapping[key] = value - } - } - - private async updateState( - type: 'request' | 'response', - event: AuthorizationEvent, - status: AuthorizationRequestStateStatus | AuthorizationResponseStateStatus - ): Promise { - if (!event.correlationId) { - throw new AriesFrameworkError(`'${type} ${status}' event without correlation id received`) - } - - try { - const eventState = { - correlationId: event.correlationId, - ...(type === 'request' ? { request: event.subject } : {}), - ...(type === 'response' ? { response: event.subject } : {}), - ...(event.error ? { error: event.error } : {}), - status, - timestamp: event.timestamp, - lastUpdated: event.timestamp, - } - if (type === 'request') { - this.authorizationRequests[event.correlationId] = eventState as AuthorizationRequestState - // We do not await these - this.updateMapping(this.nonceToCorrelationId, event, 'nonce', event.correlationId, true).catch((error) => - this.logger.error(JSON.stringify(error)) - ) - this.nonceToCorrelationId - this.updateMapping(this.stateToCorrelationId, event, 'state', event.correlationId, true).catch((error) => - this.logger.error(JSON.stringify(error)) - ) - } else { - this.authorizationResponses[event.correlationId] = eventState as AuthorizationResponseState - } - } catch (error: unknown) { - this.logger.error(`Error in update state happened: ${error}`) - } - } - - private static async cleanMappingForCorrelationId( - mapping: Record, - correlationId: string - ): Promise { - const keys = InMemoryVerifierSessionManager.getKeysForCorrelationId(mapping, correlationId) - if (keys && keys.length > 0) { - keys.forEach((key) => delete mapping[key]) - } - } - - public async deleteStateForCorrelationId(correlationId: string) { - InMemoryVerifierSessionManager.cleanMappingForCorrelationId(this.nonceToCorrelationId, correlationId).catch( - (error) => this.logger.error(JSON.stringify(error)) - ) - InMemoryVerifierSessionManager.cleanMappingForCorrelationId(this.stateToCorrelationId, correlationId).catch( - (error) => this.logger.error(JSON.stringify(error)) - ) - delete this.authorizationRequests[correlationId] - delete this.authorizationResponses[correlationId] - delete this.correlationIdToVerifyProofResponseOptions[correlationId] - } - - private async cleanup() { - const now = Date.now() - const maxAgeInMS = this.maxAgeInSeconds * 1000 - - const cleanupCorrelations = async ( - reqByCorrelationId: [string, AuthorizationRequestState | AuthorizationResponseState] - ) => { - const correlationId = reqByCorrelationId[0] - const authState = reqByCorrelationId[1] - - const ts = authState.lastUpdated || authState.timestamp - if (maxAgeInMS !== 0 && now > ts + maxAgeInMS) { - await this.deleteStateForCorrelationId(correlationId) - } - } - - const authRequests = Object.entries(this.authorizationRequests).map(cleanupCorrelations) - const authResponses = Object.entries(this.authorizationResponses).map(cleanupCorrelations) - await Promise.all([...authRequests, ...authResponses]) - } -} diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts new file mode 100644 index 0000000000..81b0134db2 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts @@ -0,0 +1,352 @@ +import type { + OpenId4VcSiopCreateAuthorizationRequestOptions, + OpenId4VcSiopCreateAuthorizationRequestReturn, + OpenId4VcSiopVerifiedAuthorizationResponse, + OpenId4VcSiopVerifyAuthorizationResponseOptions, +} from './OpenId4VcSiopVerifierServiceOptions' +import type { OpenId4VcJwtIssuer } from '../shared' +import type { AgentContext, DifPresentationExchangeDefinition } from '@aries-framework/core' +import type { PresentationVerificationCallback, SigningAlgo } from '@sphereon/did-auth-siop' + +import { + AriesFrameworkError, + DidsApi, + inject, + injectable, + InjectionSymbols, + joinUriParts, + JsonTransformer, + Logger, + SdJwtVcApi, + SignatureSuiteRegistry, + utils, + W3cCredentialService, + W3cJsonLdVerifiablePresentation, + Hasher, +} from '@aries-framework/core' +import { + AuthorizationResponse, + CheckLinkedDomain, + PassBy, + PropertyTarget, + ResponseIss, + ResponseMode, + ResponseType, + RevocationVerification, + RP, + SupportedVersion, + VerificationMode, +} from '@sphereon/did-auth-siop' + +import { storeActorIdForContextCorrelationId } from '../shared/router' +import { getVerifiablePresentationFromSphereonWrapped } from '../shared/transform' +import { + getSphereonDidResolver, + getSphereonSuppliedSignatureFromJwtIssuer, + getSupportedJwaSignatureAlgorithms, +} from '../shared/utils' + +import { OpenId4VcVerifierModuleConfig } from './OpenId4VcVerifierModuleConfig' +import { OpenId4VcVerifierRecord, OpenId4VcVerifierRepository } from './repository' + +/** + * @internal + */ +@injectable() +export class OpenId4VcSiopVerifierService { + public constructor( + @inject(InjectionSymbols.Logger) private logger: Logger, + private w3cCredentialService: W3cCredentialService, + private openId4VcVerifierRepository: OpenId4VcVerifierRepository, + private config: OpenId4VcVerifierModuleConfig + ) {} + + public async createAuthorizationRequest( + agentContext: AgentContext, + options: OpenId4VcSiopCreateAuthorizationRequestOptions & { verifier: OpenId4VcVerifierRecord } + ): Promise { + const nonce = await agentContext.wallet.generateNonce() + const state = await agentContext.wallet.generateNonce() + const correlationId = utils.uuid() + + const relyingParty = await this.getRelyingParty(agentContext, options.verifier, { + presentationDefinition: options.presentationDefinition, + requestSigner: options.requestSigner, + }) + + const authorizationRequest = await relyingParty.createAuthorizationRequest({ + correlationId, + nonce, + state, + }) + + const authorizationRequestUri = await authorizationRequest.uri() + + return { + authorizationRequestUri: authorizationRequestUri.encodedUri, + } + } + + public async verifyAuthorizationResponse( + agentContext: AgentContext, + options: OpenId4VcSiopVerifyAuthorizationResponseOptions & { verifier: OpenId4VcVerifierRecord } + ): Promise { + const authorizationResponse = await AuthorizationResponse.fromPayload(options.authorizationResponse).catch(() => { + throw new AriesFrameworkError( + `Unable to parse authorization response payload. ${JSON.stringify(options.authorizationResponse)}` + ) + }) + + const responseNonce = await authorizationResponse.getMergedProperty('nonce', { + hasher: Hasher.hash, + }) + const responseState = await authorizationResponse.getMergedProperty('state', { + hasher: Hasher.hash, + }) + const sessionManager = this.config.getSessionManager(agentContext) + + const correlationId = responseNonce + ? await sessionManager.getCorrelationIdByNonce(responseNonce, false) + : responseState + ? await sessionManager.getCorrelationIdByState(responseState, false) + : undefined + + if (!correlationId) { + throw new AriesFrameworkError( + `Unable to find correlationId for nonce '${responseNonce}' or state '${responseState}'` + ) + } + + const requestSessionState = await sessionManager.getRequestStateByCorrelationId(correlationId) + if (!requestSessionState) { + throw new AriesFrameworkError(`Unable to find request state for correlationId '${correlationId}'`) + } + + const requestClientId = await requestSessionState.request.getMergedProperty('client_id') + const requestNonce = await requestSessionState.request.getMergedProperty('nonce') + const requestState = await requestSessionState.request.getMergedProperty('state') + const presentationDefinitionsWithLocation = await requestSessionState.request.getPresentationDefinitions() + + if (!requestNonce || !requestClientId || !requestState) { + throw new AriesFrameworkError( + `Unable to find nonce, state, or client_id in authorization request for correlationId '${correlationId}'` + ) + } + + const relyingParty = await this.getRelyingParty(agentContext, options.verifier, { + presentationDefinition: presentationDefinitionsWithLocation?.[0].definition, + clientId: requestClientId, + }) + + const response = await relyingParty.verifyAuthorizationResponse(authorizationResponse.payload, { + audience: requestClientId, + correlationId, + state: requestState, + presentationDefinitions: presentationDefinitionsWithLocation, + verification: { + presentationVerificationCallback: this.getPresentationVerificationCallback(agentContext, { + nonce: requestNonce, + audience: requestClientId, + }), + // FIXME: Supplied mode is not implemented. + // See https://github.com/Sphereon-Opensource/SIOP-OID4VP/issues/55 + mode: VerificationMode.INTERNAL, + resolveOpts: { noUniversalResolverFallback: true, resolver: getSphereonDidResolver(agentContext) }, + }, + }) + + const presentationExchange = response.oid4vpSubmission + ? { + submission: response.oid4vpSubmission.submissionData, + definition: response.oid4vpSubmission.presentationDefinitions[0].definition, + presentations: response.oid4vpSubmission?.presentations.map(getVerifiablePresentationFromSphereonWrapped), + } + : undefined + + const idToken = response.authorizationResponse.idToken + ? { + payload: await response.authorizationResponse.idToken.payload(), + } + : undefined + + // TODO: do we need to verify whether idToken or vpToken is present? + // Or is that properly handled by sphereon's library? + return { + // Parameters related to ID Token. + idToken, + + // Parameters related to DIF Presentation Exchange + presentationExchange, + } + } + + public async getAllVerifiers(agentContext: AgentContext) { + return this.openId4VcVerifierRepository.getAll(agentContext) + } + + public async getByVerifierId(agentContext: AgentContext, verifierId: string) { + return this.openId4VcVerifierRepository.getByVerifierId(agentContext, verifierId) + } + + public async updateVerifier(agentContext: AgentContext, verifier: OpenId4VcVerifierRecord) { + return this.openId4VcVerifierRepository.update(agentContext, verifier) + } + + public async createVerifier(agentContext: AgentContext) { + const openId4VcVerifier = new OpenId4VcVerifierRecord({ + verifierId: utils.uuid(), + }) + + await this.openId4VcVerifierRepository.save(agentContext, openId4VcVerifier) + await storeActorIdForContextCorrelationId(agentContext, openId4VcVerifier.verifierId) + return openId4VcVerifier + } + + private async getRelyingParty( + agentContext: AgentContext, + verifier: OpenId4VcVerifierRecord, + { + presentationDefinition, + requestSigner, + clientId, + }: { + presentationDefinition?: DifPresentationExchangeDefinition + requestSigner?: OpenId4VcJwtIssuer + clientId?: string + } + ) { + const authorizationResponseUrl = joinUriParts(this.config.baseUrl, [ + verifier.verifierId, + this.config.authorizationEndpoint.endpointPath, + ]) + + const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) + + const supportedAlgs = getSupportedJwaSignatureAlgorithms(agentContext) as string[] + const supportedProofTypes = signatureSuiteRegistry.supportedProofTypes + + // Check: audience must be set to the issuer with dynamic disc otherwise self-issued.me/v2. + const builder = RP.builder() + + let _clientId = clientId + if (requestSigner) { + const suppliedSignature = await getSphereonSuppliedSignatureFromJwtIssuer(agentContext, requestSigner) + builder.withSignature(suppliedSignature) + + _clientId = suppliedSignature.did + } + + if (!_clientId) { + throw new AriesFrameworkError("Either 'requestSigner' or 'clientId' must be provided.") + } + + builder + .withRedirectUri(authorizationResponseUrl) + .withIssuer(ResponseIss.SELF_ISSUED_V2) + .withSupportedVersions([SupportedVersion.SIOPv2_D11, SupportedVersion.SIOPv2_D12_OID4VP_D18]) + // TODO: we should probably allow some dynamic values here + .withClientMetadata({ + client_id: _clientId, + passBy: PassBy.VALUE, + idTokenSigningAlgValuesSupported: supportedAlgs as SigningAlgo[], + responseTypesSupported: [ResponseType.VP_TOKEN, ResponseType.ID_TOKEN], + vpFormatsSupported: { + jwt_vc: { + alg: supportedAlgs, + }, + jwt_vc_json: { + alg: supportedAlgs, + }, + jwt_vp: { + alg: supportedAlgs, + }, + ldp_vc: { + proof_type: supportedProofTypes, + }, + ldp_vp: { + proof_type: supportedProofTypes, + }, + 'vc+sd-jwt': { + kb_jwt_alg_values: supportedAlgs, + sd_jwt_alg_values: supportedAlgs, + }, + }, + }) + .withCustomResolver(getSphereonDidResolver(agentContext)) + .withResponseMode(ResponseMode.POST) + .withResponseType(presentationDefinition ? [ResponseType.ID_TOKEN, ResponseType.VP_TOKEN] : ResponseType.ID_TOKEN) + .withScope('openid') + // TODO: support hosting requests within AFJ and passing it by reference + .withRequestBy(PassBy.VALUE) + .withCheckLinkedDomain(CheckLinkedDomain.NEVER) + // FIXME: should allow verification of revocation + // .withRevocationVerificationCallback() + .withRevocationVerification(RevocationVerification.NEVER) + .withSessionManager(this.config.getSessionManager(agentContext)) + .withEventEmitter(this.config.getEventEmitter(agentContext)) + + if (presentationDefinition) { + builder.withPresentationDefinition({ definition: presentationDefinition }, [ + PropertyTarget.REQUEST_OBJECT, + PropertyTarget.AUTHORIZATION_REQUEST, + ]) + } + + const supportedDidMethods = agentContext.dependencyManager.resolve(DidsApi).supportedResolverMethods + for (const supportedDidMethod of supportedDidMethods) { + builder.addDidMethod(supportedDidMethod) + } + + return builder.build() + } + + private getPresentationVerificationCallback( + agentContext: AgentContext, + options: { nonce: string; audience: string } + ): PresentationVerificationCallback { + return async (encodedPresentation, presentationSubmission) => { + this.logger.debug(`Presentation response`, JsonTransformer.toJSON(encodedPresentation)) + this.logger.debug(`Presentation submission`, presentationSubmission) + + if (!encodedPresentation) throw new AriesFrameworkError('Did not receive a presentation for verification.') + + // TODO: it might be better here to look at the presentation submission to know + // If presentation includes a ~, we assume it's an SD-JWT-VC + if (typeof encodedPresentation === 'string' && encodedPresentation.includes('~')) { + const sdJwtVcApi = agentContext.dependencyManager.resolve(SdJwtVcApi) + + const verificationResult = await sdJwtVcApi.verify({ + compactSdJwtVc: encodedPresentation, + keyBinding: { + audience: options.audience, + nonce: options.nonce, + }, + }) + + return { + verified: verificationResult.verification.isValid, + } + } else if (typeof encodedPresentation === 'string') { + const verificationResult = await this.w3cCredentialService.verifyPresentation(agentContext, { + presentation: encodedPresentation, + challenge: options.nonce, + domain: options.audience, + }) + + return { + verified: verificationResult.isValid, + } + } else { + const verificationResult = await this.w3cCredentialService.verifyPresentation(agentContext, { + presentation: JsonTransformer.fromJSON(encodedPresentation, W3cJsonLdVerifiablePresentation), + challenge: options.nonce, + domain: options.audience, + }) + + return { + verified: verificationResult.isValid, + } + } + } + } +} diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierServiceOptions.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierServiceOptions.ts new file mode 100644 index 0000000000..763c64b750 --- /dev/null +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierServiceOptions.ts @@ -0,0 +1,51 @@ +import type { + OpenId4VcJwtIssuer, + OpenId4VcSiopAuthorizationResponsePayload, + OpenId4VcSiopIdTokenPayload, +} from '../shared' +import type { + DifPresentationExchangeDefinition, + DifPresentationExchangeSubmission, + W3cVerifiablePresentation, + SdJwtVc, + DifPresentationExchangeDefinitionV2, +} from '@aries-framework/core' + +export interface OpenId4VcSiopCreateAuthorizationRequestOptions { + /** + * Signing information for the request JWT. This will be used to sign the request JWT + * and to set the client_id for registration of client_metadata. + */ + requestSigner: OpenId4VcJwtIssuer + + /** + * A DIF Presentation Definition (v2) can be provided to request a Verifiable Presentation using OpenID4VP. + */ + presentationDefinition?: DifPresentationExchangeDefinitionV2 +} + +export interface OpenId4VcSiopVerifyAuthorizationResponseOptions { + /** + * The authorization response received from the OpenID Provider (OP). + */ + authorizationResponse: OpenId4VcSiopAuthorizationResponsePayload +} + +export interface OpenId4VcSiopCreateAuthorizationRequestReturn { + authorizationRequestUri: string +} + +/** + * Either `idToken` and/or `presentationExchange` will be present, but not none. + */ +export interface OpenId4VcSiopVerifiedAuthorizationResponse { + idToken?: { + payload: OpenId4VcSiopIdTokenPayload + } + + presentationExchange?: { + submission: DifPresentationExchangeSubmission + definition: DifPresentationExchangeDefinition + presentations: Array + } +} diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierApi.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierApi.ts index 46efcc3d2c..ca746f6604 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierApi.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierApi.ts @@ -1,14 +1,14 @@ import type { - OpenId4VcCreateAuthorizationRequestOptions, - OpenId4VcVerifyAuthorizationResponseOptions, - OpenId4VcAuthorizationRequestWithMetadata, - VerifiedOpenId4VcAuthorizationResponse, -} from './OpenId4VcVerifierServiceOptions' + OpenId4VcSiopCreateAuthorizationRequestOptions, + OpenId4VcSiopVerifyAuthorizationResponseOptions, + OpenId4VcSiopCreateAuthorizationRequestReturn, + OpenId4VcSiopVerifiedAuthorizationResponse, +} from './OpenId4VcSiopVerifierServiceOptions' import { injectable, AgentContext } from '@aries-framework/core' +import { OpenId4VcSiopVerifierService } from './OpenId4VcSiopVerifierService' import { OpenId4VcVerifierModuleConfig } from './OpenId4VcVerifierModuleConfig' -import { OpenId4VcVerifierService } from './OpenId4VcVerifierService' /** * @public @@ -18,28 +18,28 @@ export class OpenId4VcVerifierApi { public constructor( public readonly config: OpenId4VcVerifierModuleConfig, private agentContext: AgentContext, - private openId4VcVerifierService: OpenId4VcVerifierService + private openId4VcSiopVerifierService: OpenId4VcSiopVerifierService ) {} /** * Retrieve all verifier records from storage */ public async getAllVerifiers() { - return this.openId4VcVerifierService.getAllVerifiers(this.agentContext) + return this.openId4VcSiopVerifierService.getAllVerifiers(this.agentContext) } /** * Retrieve a verifier record from storage by its verified id */ public async getByVerifierId(verifierId: string) { - return this.openId4VcVerifierService.getByVerifierId(this.agentContext, verifierId) + return this.openId4VcSiopVerifierService.getByVerifierId(this.agentContext, verifierId) } /** * Create a new verifier and store the new verifier record. */ public async createVerifier() { - return this.openId4VcVerifierService.createVerifier(this.agentContext) + return this.openId4VcSiopVerifierService.createVerifier(this.agentContext) } /** @@ -51,16 +51,16 @@ export class OpenId4VcVerifierApi { * * Other flows (non-SIOP) are not supported at the moment, but can be added in the future. * - * See {@link OpenId4VcCreateAuthorizationRequestOptions} for detailed documentation on the options. + * See {@link OpenId4VcSiopCreateAuthorizationRequestOptions} for detailed documentation on the options. */ public async createAuthorizationRequest({ verifierId, ...otherOptions - }: OpenId4VcCreateAuthorizationRequestOptions & { + }: OpenId4VcSiopCreateAuthorizationRequestOptions & { verifierId: string - }): Promise { + }): Promise { const verifier = await this.getByVerifierId(verifierId) - return await this.openId4VcVerifierService.createAuthorizationRequest(this.agentContext, { + return await this.openId4VcSiopVerifierService.createAuthorizationRequest(this.agentContext, { ...otherOptions, verifier, }) @@ -75,11 +75,11 @@ export class OpenId4VcVerifierApi { public async verifyAuthorizationResponse({ verifierId, ...otherOptions - }: OpenId4VcVerifyAuthorizationResponseOptions & { + }: OpenId4VcSiopVerifyAuthorizationResponseOptions & { verifierId: string - }): Promise { + }): Promise { const verifier = await this.getByVerifierId(verifierId) - return await this.openId4VcVerifierService.verifyAuthorizationResponse(this.agentContext, { + return await this.openId4VcSiopVerifierService.verifyAuthorizationResponse(this.agentContext, { ...otherOptions, verifier, }) diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts index def9139dac..9ed1b250b3 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts @@ -8,7 +8,7 @@ import { getAgentContextForActorId, getRequestContext, importExpress } from '../ import { OpenId4VcVerifierApi } from './OpenId4VcVerifierApi' import { OpenId4VcVerifierModuleConfig } from './OpenId4VcVerifierModuleConfig' -import { OpenId4VcVerifierService } from './OpenId4VcVerifierService' +import { OpenId4VcSiopVerifierService } from './OpenId4VcSiopVerifierService' import { OpenId4VcVerifierRepository } from './repository' import { configureAuthorizationEndpoint } from './router' @@ -40,7 +40,7 @@ export class OpenId4VcVerifierModule implements Module { dependencyManager.registerContextScoped(OpenId4VcVerifierApi) // Services - dependencyManager.registerSingleton(OpenId4VcVerifierService) + dependencyManager.registerSingleton(OpenId4VcSiopVerifierService) // Repository dependencyManager.registerSingleton(OpenId4VcVerifierRepository) diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts index 05a49a32d1..7ede42f371 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModuleConfig.ts @@ -1,14 +1,12 @@ -import type { AuthorizationEndpointConfig } from './router/authorizationEndpoint' -import type { Optional, AgentContext } from '@aries-framework/core' +import type { OpenId4VcSiopAuthorizationEndpointConfig } from './router/authorizationEndpoint' +import type { Optional, AgentContext, AgentDependencies } from '@aries-framework/core' +import type { IRPSessionManager } from '@sphereon/did-auth-siop' import type { Router } from 'express' -import { AgentConfig } from '@aries-framework/core' -import { EventEmitter } from 'events' +import { InMemoryRPSessionManager } from '@sphereon/did-auth-siop' import { importExpress } from '../shared/router' -import { InMemoryVerifierSessionManager, type IInMemoryVerifierSessionManager } from './InMemoryVerifierSessionManager' - export interface OpenId4VcVerifierModuleConfigOptions { /** * Base url at which the verifier endpoints will be hosted. All endpoints will be exposed with @@ -26,20 +24,16 @@ export interface OpenId4VcVerifierModuleConfigOptions { router?: Router endpoints?: { - // FIXME: interface name with openid4vc prefix - authorization?: Optional + authorization?: Optional } - - // FIXME: remove - sessionManagerFactory?: () => IInMemoryVerifierSessionManager } export class OpenId4VcVerifierModuleConfig { private options: OpenId4VcVerifierModuleConfigOptions public readonly router: Router - private eventEmitterMap: Map - private sessionManagerMap: Map + private eventEmitterMap: Map> + private sessionManagerMap: Map public constructor(options: OpenId4VcVerifierModuleConfigOptions) { this.options = options @@ -53,7 +47,7 @@ export class OpenId4VcVerifierModuleConfig { return this.options.baseUrl } - public get authorizationEndpoint(): AuthorizationEndpointConfig { + public get authorizationEndpoint(): OpenId4VcSiopAuthorizationEndpointConfig { // Use user supplied options, or return defaults. const userOptions = this.options.endpoints?.authorization @@ -63,25 +57,27 @@ export class OpenId4VcVerifierModuleConfig { } } + // FIXME: rework (no in-memory) public getSessionManager(agentContext: AgentContext) { const val = this.sessionManagerMap.get(agentContext.contextCorrelationId) if (val) return val - const logger = agentContext.dependencyManager.resolve(AgentConfig).logger + const eventEmitter = this.getEventEmitter(agentContext) - const newVal = - this.options.sessionManagerFactory?.() ?? - new InMemoryVerifierSessionManager(this.getEventEmitter(agentContext), logger) + const newVal = new InMemoryRPSessionManager(eventEmitter) this.sessionManagerMap.set(agentContext.contextCorrelationId, newVal) return newVal } - public getEventEmitter(agentConext: AgentContext) { - const val = this.eventEmitterMap.get(agentConext.contextCorrelationId) + // FIXME: rework (no-memory) + public getEventEmitter(agentContext: AgentContext) { + const EventEmitterClass = agentContext.config.agentDependencies.EventEmitterClass + + const val = this.eventEmitterMap.get(agentContext.contextCorrelationId) if (val) return val - const newVal = new EventEmitter() - this.eventEmitterMap.set(agentConext.contextCorrelationId, newVal) + const newVal = new EventEmitterClass() + this.eventEmitterMap.set(agentContext.contextCorrelationId, newVal) return newVal } } diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierService.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierService.ts deleted file mode 100644 index 761f8bcd30..0000000000 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierService.ts +++ /dev/null @@ -1,382 +0,0 @@ -import type { - OpenId4VcAuthorizationRequestWithMetadata, - OpenId4VcCreateAuthorizationRequestOptions, - OpenId4VcAuthorizationRequestMetadata, - VerifiedOpenId4VcAuthorizationResponse, - HolderMetadata, - OpenId4VcVerifyAuthorizationResponseOptions, -} from './OpenId4VcVerifierServiceOptions' -import type { AgentContext, W3cVerifyPresentationResult } from '@aries-framework/core' -import type { PresentationVerificationCallback, SigningAlgo } from '@sphereon/did-auth-siop' - -import { - utils, - joinUriParts, - InjectionSymbols, - Logger, - W3cCredentialService, - inject, - injectable, - AriesFrameworkError, - W3cJsonLdVerifiablePresentation, - JsonTransformer, -} from '@aries-framework/core' -import { - RP, - ResponseIss, - RevocationVerification, - SupportedVersion, - ResponseMode, - PropertyTarget, - ResponseType, - CheckLinkedDomain, - PresentationDefinitionLocation, - PassBy, - VerificationMode, - AuthorizationResponse, -} from '@sphereon/did-auth-siop' - -import { storeActorIdForContextCorrelationId } from '../shared/router' -import { getVerifiablePresentationFromSphereonWrapped } from '../shared/transform' -import { - getSupportedDidMethods, - getSuppliedSignatureFromVerificationMethod, - getResolver, - getSupportedJwaSignatureAlgorithms, -} from '../shared/utils' - -import { OpenId4VcVerifierModuleConfig } from './OpenId4VcVerifierModuleConfig' -import { OpenId4VcVerifierRecord, OpenId4VcVerifierRepository } from './repository' -import { openidStaticOpConfiguration, siopv2StaticOpConfiguration } from './staticOpConfiguration' - -/** - * @internal - */ -@injectable() -export class OpenId4VcVerifierService { - public constructor( - @inject(InjectionSymbols.Logger) private logger: Logger, - private w3cCredentialService: W3cCredentialService, - private openId4VcVerifierRepository: OpenId4VcVerifierRepository, - private config: OpenId4VcVerifierModuleConfig - ) {} - - public async createAuthorizationRequest( - agentContext: AgentContext, - options: OpenId4VcCreateAuthorizationRequestOptions & { verifier: OpenId4VcVerifierRecord } - ): Promise { - const nonce = await agentContext.wallet.generateNonce() - const state = await agentContext.wallet.generateNonce() - const correlationId = utils.uuid() - - const relyingParty = await this.getRelyingParty(agentContext, options.verifier, options) - const authorizationRequest = await relyingParty.createAuthorizationRequest({ - correlationId, - nonce, - state, - }) - - const authorizationRequestUri = await authorizationRequest.uri() - const encodedAuthorizationRequestUri = authorizationRequestUri.encodedUri - - const metadata = { - nonce, - correlationId, - state, - } - - // FIXME: we need to store some state here? - // Why is the sphereon session manage not enough? - await this.config.getSessionManager(agentContext).saveVerifyProofResponseOptions(correlationId, { - createProofRequestOptions: options, - proofRequestMetadata: metadata, - }) - - return { - authorizationRequestUri: encodedAuthorizationRequestUri, - metadata, - } - } - - public async verifyAuthorizationResponse( - agentContext: AgentContext, - options: OpenId4VcVerifyAuthorizationResponseOptions & { verifier: OpenId4VcVerifierRecord } - ): Promise { - const authorizationResponse = await AuthorizationResponse.fromPayload(options.authorizationResponse).catch(() => { - throw new AriesFrameworkError( - `Unable to parse authorization response payload. ${JSON.stringify(options.authorizationResponse)}` - ) - }) - - // FIXME: we need to rework the custom verification state stuff - const resNonce = await authorizationResponse.getMergedProperty('nonce', false) - const resState = await authorizationResponse.getMergedProperty('state', false) - const sessionManager = this.config.getSessionManager(agentContext) - - const correlationId = resNonce - ? await sessionManager.getCorrelationIdByNonce(resNonce, false) - : resState - ? await sessionManager.getCorrelationIdByState(resState, false) - : undefined - - if (!correlationId) { - throw new AriesFrameworkError(`Unable to find correlationId for nonce '${resNonce}' or state '${resState}'`) - } - - const verifyProofResponseOptions = await sessionManager.getVerifyProofResponseOptions(correlationId) - if (!verifyProofResponseOptions) { - throw new AriesFrameworkError(`Unable to associate a request to the response correlationId '${correlationId}'`) - } - - const { createProofRequestOptions, proofRequestMetadata } = verifyProofResponseOptions - const presentationDefinition = createProofRequestOptions.presentationDefinition - - // For now we always use the VP_TOKEN - const presentationDefinitionsWithLocation = presentationDefinition - ? [{ definition: presentationDefinition, location: PresentationDefinitionLocation.CLAIMS_VP_TOKEN }] - : undefined - - const relyingParty = await this.getRelyingParty( - agentContext, - options.verifier, - createProofRequestOptions, - proofRequestMetadata - ) - - const response = await relyingParty.verifyAuthorizationResponse(authorizationResponse.payload, { - // FIXME: can be extracted from iss of request? - audience: createProofRequestOptions.verificationMethod.id, - correlationId, - // FIXME: can be extracted from request? - nonce: proofRequestMetadata.nonce, - // FIXME: can be extracted from request? - state: proofRequestMetadata.state, - presentationDefinitions: presentationDefinitionsWithLocation, - // FIXME: does this verify the VP as well? Or just the id_token and the vp_token submission, - // but not the actual signature on the VP? - // -> I think it uses the presentation verification callback - verification: { - mode: VerificationMode.INTERNAL, - resolveOpts: { noUniversalResolverFallback: true, resolver: getResolver(agentContext) }, - }, - }) - - const presentationExchange = response.oid4vpSubmission - ? { - submission: response.oid4vpSubmission?.submissionData, - definitions: response.oid4vpSubmission?.presentationDefinitions.map((d) => d.definition), - presentations: response.oid4vpSubmission?.presentations.map(getVerifiablePresentationFromSphereonWrapped), - } - : undefined - - return { - // FIXME: rename and only extract needed payload, don't want sphereon types to be exposed on AFJ layer - idTokenPayload: await response.authorizationResponse.idToken.payload(), - - // Parameters related to DIF Presentation Exchange - presentationExchange, - } - } - - public async getAllVerifiers(agentContext: AgentContext) { - return this.openId4VcVerifierRepository.getAll(agentContext) - } - - public async getByVerifierId(agentContext: AgentContext, verifierId: string) { - return this.openId4VcVerifierRepository.getByVerifierId(agentContext, verifierId) - } - - public async updateVerifier(agentContext: AgentContext, verifier: OpenId4VcVerifierRecord) { - return this.openId4VcVerifierRepository.update(agentContext, verifier) - } - - public async createVerifier(agentContext: AgentContext) { - const openId4VcVerifier = new OpenId4VcVerifierRecord({ - verifierId: utils.uuid(), - }) - - await this.openId4VcVerifierRepository.save(agentContext, openId4VcVerifier) - await storeActorIdForContextCorrelationId(agentContext, openId4VcVerifier.verifierId) - return openId4VcVerifier - } - - private async getRelyingParty( - agentContext: AgentContext, - verifier: OpenId4VcVerifierRecord, - createAuthorizationRequestOptions: OpenId4VcCreateAuthorizationRequestOptions, - proofRequestMetadata?: OpenId4VcAuthorizationRequestMetadata - ) { - const { verificationEndpointUrl, presentationDefinition, verificationMethod } = createAuthorizationRequestOptions - - const isVpRequest = presentationDefinition !== undefined - const openIdConfiguration = this.getOpenIdConfiguration(createAuthorizationRequestOptions) - - const { signature, did, kid, alg } = await getSuppliedSignatureFromVerificationMethod( - agentContext, - verificationMethod - ) - - // Check if the OpenId Provider can validate the request signature provided by the Relying Party - const requestObjectSigningAlgValuesSupported = openIdConfiguration.requestObjectSigningAlgValuesSupported - if (requestObjectSigningAlgValuesSupported && !requestObjectSigningAlgValuesSupported.includes(alg)) { - throw new AriesFrameworkError( - [ - `Cannot sign authorization request with '${alg}' that isn't supported by the OpenId Provider.`, - `Supported algorithms are ${requestObjectSigningAlgValuesSupported}`, - ].join('\n') - ) - } - - // Check if the Relying Party (Verifier) can validate the IdToken provided by the OpenId Provider (Holder) - // FIXME: This might cause issues as the static configuration is very limited and thus it would - // prevent any new algorithms from being used. - const idTokenSigningAlgValuesSupported = openIdConfiguration.idTokenSigningAlgValuesSupported - if (idTokenSigningAlgValuesSupported) { - const rpSupportedSignatureAlgorithms = getSupportedJwaSignatureAlgorithms( - agentContext - ) as unknown as SigningAlgo[] - - const possibleIdTokenSigningAlgValue = Array.isArray(idTokenSigningAlgValuesSupported) - ? idTokenSigningAlgValuesSupported.some((value) => rpSupportedSignatureAlgorithms.includes(value)) - : rpSupportedSignatureAlgorithms.includes(idTokenSigningAlgValuesSupported) - - if (!possibleIdTokenSigningAlgValue) { - throw new AriesFrameworkError( - [ - `The OpenId Provider supports no signature algorithms that are supported by the Relying Party.`, - `Relying Party supported algorithms are ${rpSupportedSignatureAlgorithms}.`, - `OpenId Provider supported algorithms are ${idTokenSigningAlgValuesSupported}.`, - ].join('\n') - ) - } - } - - // FIXME: what do we call this? RP url? There should be an openid name for it - const relyingPartyUrl = joinUriParts(this.config.baseUrl, [verifier.verifierId]) - // FIXME: is it authorization endpoint? What do you call the endpoint where you - // submit the authorization response to? - const redirectUri = - verificationEndpointUrl ?? joinUriParts(relyingPartyUrl, [this.config.authorizationEndpoint.endpointPath]) - - // Check: audience must be set to the issuer with dynamic disc otherwise self-issued.me/v2. - const builder = RP.builder() - .withClientId(verificationMethod.id) - .withRedirectUri(redirectUri) - .withIssuer(ResponseIss.SELF_ISSUED_V2) - .withSuppliedSignature(signature, did, kid, alg) - .withSupportedVersions([SupportedVersion.SIOPv2_D11, SupportedVersion.SIOPv2_D12_OID4VP_D18]) - // FIXME: client metadata is the metadata of the RP - // but it's being added as OP metadata - // Is this both OP and Client metadata? - // RP = Client = verifier - // OP = holder - .withClientMetadata(holderMetadata) - .withCustomResolver(getResolver(agentContext)) - .withResponseMode(ResponseMode.POST) - .withResponseType(isVpRequest ? [ResponseType.ID_TOKEN, ResponseType.VP_TOKEN] : ResponseType.ID_TOKEN) - .withScope('openid') - .withRequestBy(PassBy.VALUE) - .withCheckLinkedDomain(CheckLinkedDomain.NEVER) - // FIXME: should allow verification of revocation - // .withRevocationVerificationCallback() - .withRevocationVerification(RevocationVerification.NEVER) - .withSessionManager(this.config.getSessionManager(agentContext)) - .withEventEmitter(this.config.getEventEmitter(agentContext)) - - if (openIdConfiguration.authorization_endpoint) { - builder.withAuthorizationEndpoint(openIdConfiguration.authorization_endpoint) - } - - // FIXME: we don't want to pass proof request metadata - // Maybe we can just return the builder, and add it in the caller method? - // Or we somehow dynamically get the nonce from the request? - if (proofRequestMetadata) { - builder.withPresentationVerification( - this.getPresentationVerificationCallback(agentContext, { challenge: proofRequestMetadata.nonce }) - ) - } - - if (isVpRequest) { - builder.withPresentationDefinition({ definition: presentationDefinition }, [ - PropertyTarget.REQUEST_OBJECT, - PropertyTarget.AUTHORIZATION_REQUEST, - ]) - } - - const supportedDidMethods = getSupportedDidMethods(agentContext) - for (const supportedDidMethod of supportedDidMethods) { - builder.addDidMethod(supportedDidMethod) - } - - return builder.build() - } - - // FIXME: does the higher level check whether the iss of the VP is ok? - private getPresentationVerificationCallback( - agentContext: AgentContext, - options: { challenge: string } - ): PresentationVerificationCallback { - const { challenge } = options - return async (encodedPresentation, presentationSubmission) => { - this.logger.debug(`Presentation response`, JsonTransformer.toJSON(encodedPresentation)) - this.logger.debug(`Presentation submission`, presentationSubmission) - - if (!encodedPresentation) throw new AriesFrameworkError('Did not receive a presentation for verification.') - - let verificationResult: W3cVerifyPresentationResult - if (typeof encodedPresentation === 'string') { - verificationResult = await this.w3cCredentialService.verifyPresentation(agentContext, { - presentation: encodedPresentation, - challenge, - }) - } else { - verificationResult = await this.w3cCredentialService.verifyPresentation(agentContext, { - presentation: JsonTransformer.fromJSON(encodedPresentation, W3cJsonLdVerifiablePresentation), - challenge, - }) - } - - return { verified: verificationResult.isValid } - } - } - - private getOpenIdConfiguration(options: OpenId4VcCreateAuthorizationRequestOptions): HolderMetadata { - const isVpRequest = options.presentationDefinition !== undefined - - // Not provided, use default static configurations - if (!options.openIdProvider) { - return isVpRequest ? openidStaticOpConfiguration : siopv2StaticOpConfiguration - } - - // siopv2: provided or not provided and not vp request - if (options.openIdProvider === 'siopv2:') { - if (isVpRequest) { - throw new AriesFrameworkError( - "Cannot use 'siopv2:' as OP configuration when a presentation definition is provided. Use 'openid:' instead." - ) - } - return siopv2StaticOpConfiguration - } - - // openid: provided or not provided and vp request - if (options.openIdProvider === 'openid:') { - return openidStaticOpConfiguration - } - - // if string it MUST be an url - if (typeof options.openIdProvider === 'string') { - // TODO: add url validation - const referenceUri = options.openIdProvider.includes('/.well-known/openid-configuration') - ? options.openIdProvider - : joinUriParts(options.openIdProvider, ['/.well-known/openid-configuration']) - - return { - reference_uri: referenceUri, - passBy: PassBy.REFERENCE, - targets: PropertyTarget.REQUEST_OBJECT, - } - } - - return options.openIdProvider - } -} diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierServiceOptions.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierServiceOptions.ts deleted file mode 100644 index 64ea6d387b..0000000000 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierServiceOptions.ts +++ /dev/null @@ -1,91 +0,0 @@ -import type { - DifPresentationExchangeDefinition, - DifPresentationExchangeSubmission, - VerificationMethod, - W3cVerifiablePresentation, - SdJwtVc, -} from '@aries-framework/core' -import type { - IDTokenPayload, - VerifiedOpenID4VPSubmission, - ClientMetadataOpts, - AuthorizationResponsePayload, -} from '@sphereon/did-auth-siop' -import type { PresentationDefinitionV1, PresentationDefinitionV2 } from '@sphereon/pex-models' - -export { PassBy, SigningAlgo, SubjectType, ResponseType, Scope } from '@sphereon/did-auth-siop' - -export type HolderMetadata = ClientMetadataOpts & { authorization_endpoint?: string } - -export type { PresentationDefinitionV1, PresentationDefinitionV2, VerifiedOpenID4VPSubmission, IDTokenPayload } - -export interface OpenId4VcCreateAuthorizationRequestOptions { - /** - * FIXME: should be a string or a VerificationMethod instance (or something we configure in the record?) - * The VerificationMethod used for signing the proof request. - */ - verificationMethod: VerificationMethod - - /** - * FIXME: rework - * The URL to where the holder will send the response. - */ - verificationEndpointUrl?: string - - /** - * The OpenID Provider (OP) configuration can be provided in three different ways: - * - Statically, by providing the configuration as an object conforming to {@link HolderMetadata} in the `options.openIdProvider` parameter. - * - Dynamically, by providing the openid `issuer` URL in the `options.openIdProvider` parameter. The metadata will be retrieved - * from the hosted OpenID configuration endpoint. - * - Using a static configuration as defined in [SIOPv2](https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-static-configuration-values). The following - * static configurations are supported, identified by the value of the `authorization_endpoint`: - * - `siopv2:` - Supporting `id_token` as a `response_type`. See [`siopv2:`](https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-a-set-of-static-configurati) - * - `openid:` - Supporting both `vp_token` and `id_token` as a `response_type`. See [`openid:`](https://openid.net/specs/openid-connect-self-issued-v2-1_0.html#name-a-set-of-static-configuratio) - * - * Note that `siopv2:` CAN NOT be used if a presentation definition is provided in the `presentationDefinition` parameter. If no value is supplied, the following defaults will be used as the `openIdProvider`: - * - `siopv2:` - If no presentation definition is provided - * - `openid:` - If a presentation definition is provided. - */ - openIdProvider?: HolderMetadata | string - - /** - * A DIF Presentation Definition (v2) can be provided to request a Verifiable Presentation using OpenID4VP. - */ - presentationDefinition?: PresentationDefinitionV2 -} - -export interface OpenId4VcVerifyAuthorizationResponseOptions { - /** - * The authorization response received from the OpenID Provider (OP). - */ - authorizationResponse: OpenId4VcAuthorizationResponse -} - -export interface OpenId4VcAuthorizationRequestMetadata { - correlationId: string - nonce: string - state: string -} - -export interface OpenId4VcAuthorizationRequestWithMetadata { - authorizationRequestUri: string - metadata: OpenId4VcAuthorizationRequestMetadata -} - -export interface VerifyProofResponseOptions { - createProofRequestOptions: OpenId4VcCreateAuthorizationRequestOptions - proofRequestMetadata: OpenId4VcAuthorizationRequestMetadata -} - -export interface VerifiedOpenId4VcAuthorizationResponse { - idTokenPayload: IDTokenPayload - presentationExchange: - | { - submission: DifPresentationExchangeSubmission - definitions: DifPresentationExchangeDefinition[] - presentations: Array - } - | undefined -} - -export type OpenId4VcAuthorizationResponse = AuthorizationResponsePayload diff --git a/packages/openid4vc/src/openid4vc-verifier/__tests__/openId4vc-verifier-module.test.ts b/packages/openid4vc/src/openid4vc-verifier/__tests__/openId4vc-verifier-module.test.ts index c45cb612ec..cf9becd453 100644 --- a/packages/openid4vc/src/openid4vc-verifier/__tests__/openId4vc-verifier-module.test.ts +++ b/packages/openid4vc/src/openid4vc-verifier/__tests__/openId4vc-verifier-module.test.ts @@ -3,7 +3,7 @@ import type { DependencyManager } from '@aries-framework/core' import { OpenId4VcVerifierApi } from '../OpenId4VcVerifierApi' import { OpenId4VcVerifierModule } from '../OpenId4VcVerifierModule' -import { OpenId4VcVerifierService } from '../OpenId4VcVerifierService' +import { OpenId4VcSiopVerifierService } from '../OpenId4VcSiopVerifierService' const dependencyManager = { registerInstance: jest.fn(), @@ -28,6 +28,6 @@ describe('OpenId4VcVerifierModule', () => { expect(dependencyManager.registerContextScoped).toHaveBeenCalledWith(OpenId4VcVerifierApi) expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(1) - expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VcVerifierService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VcSiopVerifierService) }) }) diff --git a/packages/openid4vc/src/openid4vc-verifier/index.ts b/packages/openid4vc/src/openid4vc-verifier/index.ts index 24bc2e517c..7c8fd8a4b1 100644 --- a/packages/openid4vc/src/openid4vc-verifier/index.ts +++ b/packages/openid4vc/src/openid4vc-verifier/index.ts @@ -1,6 +1,5 @@ export * from './OpenId4VcVerifierApi' export * from './OpenId4VcVerifierModule' -export * from './OpenId4VcVerifierService' -export * from './OpenId4VcVerifierServiceOptions' +export * from './OpenId4VcSiopVerifierService' +export * from './OpenId4VcSiopVerifierServiceOptions' export * from './OpenId4VcVerifierModuleConfig' -export * from './InMemoryVerifierSessionManager' diff --git a/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts b/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts index 2b73927f92..6bafb8236f 100644 --- a/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts +++ b/packages/openid4vc/src/openid4vc-verifier/router/authorizationEndpoint.ts @@ -3,9 +3,9 @@ import type { AuthorizationResponsePayload } from '@sphereon/did-auth-siop' import type { Router, Response } from 'express' import { getRequestContext, sendErrorResponse } from '../../shared/router' -import { OpenId4VcVerifierService } from '../OpenId4VcVerifierService' +import { OpenId4VcSiopVerifierService } from '../OpenId4VcSiopVerifierService' -export interface AuthorizationEndpointConfig { +export interface OpenId4VcSiopAuthorizationEndpointConfig { /** * The path at which the authorization endpoint should be made available. Note that it will be * hosted at a subpath to take into account multiple tenants and verifiers. @@ -15,18 +15,18 @@ export interface AuthorizationEndpointConfig { endpointPath: string } -export function configureAuthorizationEndpoint(router: Router, config: AuthorizationEndpointConfig) { +export function configureAuthorizationEndpoint(router: Router, config: OpenId4VcSiopAuthorizationEndpointConfig) { router.post(config.endpointPath, async (request: OpenId4VcVerificationRequest, response: Response, next) => { const { agentContext, verifier } = getRequestContext(request) try { - const openId4VcVerifierService = agentContext.dependencyManager.resolve(OpenId4VcVerifierService) + const openId4VcVerifierService = agentContext.dependencyManager.resolve(OpenId4VcSiopVerifierService) const isVpRequest = request.body.presentation_submission !== undefined const authorizationResponse: AuthorizationResponsePayload = request.body if (isVpRequest) authorizationResponse.presentation_submission = JSON.parse(request.body.presentation_submission) - // FIXME: we should emit an event here + // FIXME: we should emit an event here and in other places await openId4VcVerifierService.verifyAuthorizationResponse(agentContext, { authorizationResponse: request.body, verifier, diff --git a/packages/openid4vc/src/openid4vc-verifier/staticOpConfiguration.ts b/packages/openid4vc/src/openid4vc-verifier/staticOpConfiguration.ts deleted file mode 100644 index d5a526b3b7..0000000000 --- a/packages/openid4vc/src/openid4vc-verifier/staticOpConfiguration.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { HolderMetadata } from './OpenId4VcVerifierServiceOptions' - -import { ResponseType, Scope, SubjectType, SigningAlgo, PassBy } from '@sphereon/did-auth-siop' - -export const siopv2StaticOpConfiguration: HolderMetadata = { - authorization_endpoint: 'siopv2:', - subject_syntax_types_supported: ['urn:ietf:params:oauth:jwk-thumbprint'], - responseTypesSupported: [ResponseType.ID_TOKEN], - scopesSupported: [Scope.OPENID], - subjectTypesSupported: [SubjectType.PAIRWISE], - idTokenSigningAlgValuesSupported: [SigningAlgo.ES256], - requestObjectSigningAlgValuesSupported: [SigningAlgo.ES256], - passBy: PassBy.VALUE, -} - -export const openidStaticOpConfiguration: HolderMetadata = { - authorization_endpoint: 'openid:', - subject_syntax_types_supported: ['urn:ietf:params:oauth:jwk-thumbprint'], - responseTypesSupported: [ResponseType.ID_TOKEN, ResponseType.VP_TOKEN], - scopesSupported: [Scope.OPENID], - subjectTypesSupported: [SubjectType.PAIRWISE], - idTokenSigningAlgValuesSupported: [SigningAlgo.ES256], - requestObjectSigningAlgValuesSupported: [SigningAlgo.ES256], - passBy: PassBy.VALUE, - vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.ES256] }, jwt_vp: { alg: [SigningAlgo.ES256] } }, -} diff --git a/packages/openid4vc/src/shared/models/OpenId4VcJwtIssuer.ts b/packages/openid4vc/src/shared/models/OpenId4VcJwtIssuer.ts new file mode 100644 index 0000000000..5165628db1 --- /dev/null +++ b/packages/openid4vc/src/shared/models/OpenId4VcJwtIssuer.ts @@ -0,0 +1,13 @@ +interface OpenId4VcJwtIssuerDid { + method: 'did' + didUrl: string +} + +// TODO: enable once supported in sphereon lib +// See https://github.com/Sphereon-Opensource/SIOP-OID4VP/issues/67 +// interface OpenId4VcJwtIssuerJwk { +// method: 'jwk' +// jwk: Jwk +// } + +export type OpenId4VcJwtIssuer = OpenId4VcJwtIssuerDid diff --git a/packages/openid4vc/src/shared/models/index.ts b/packages/openid4vc/src/shared/models/index.ts index a20434cf4c..7f91d0a78c 100644 --- a/packages/openid4vc/src/shared/models/index.ts +++ b/packages/openid4vc/src/shared/models/index.ts @@ -1,3 +1,8 @@ +import type { + VerifiedAuthorizationRequest, + AuthorizationResponsePayload, + IDTokenPayload, +} from '@sphereon/did-auth-siop' import type { AssertedUniformCredentialOffer, CredentialIssuerMetadata, @@ -21,5 +26,10 @@ export type OpenId4VciCredentialRequestSdJwtVc = CredentialRequestSdJwtVc export type OpenId4VciCredentialOffer = AssertedUniformCredentialOffer export type OpenId4VciCredentialOfferPayload = CredentialOfferPayloadV1_0_11 +export type OpenId4VcSiopVerifiedAuthorizationRequest = VerifiedAuthorizationRequest +export type OpenId4VcSiopAuthorizationResponsePayload = AuthorizationResponsePayload +export type OpenId4VcSiopIdTokenPayload = IDTokenPayload + +export * from './OpenId4VcJwtIssuer' export * from './CredentialHolderBinding' export * from './OpenId4VciCredentialFormatProfile' diff --git a/packages/openid4vc/src/shared/transform.ts b/packages/openid4vc/src/shared/transform.ts index 62e4b6f891..c45d0fe793 100644 --- a/packages/openid4vc/src/shared/transform.ts +++ b/packages/openid4vc/src/shared/transform.ts @@ -1,6 +1,7 @@ import type { W3cVerifiableCredential, W3cVerifiablePresentation, SdJwtVc } from '@aries-framework/core' import type { W3CVerifiableCredential as SphereonW3cVerifiableCredential, + W3CVerifiablePresentation as SphereonW3cVerifiablePresentation, CompactSdJwtVc as SphereonCompactSdJwtVc, WrappedVerifiablePresentation, } from '@sphereon/ssi-types' @@ -30,6 +31,21 @@ export function getSphereonVerifiableCredential( } } +export function getSphereonVerifiablePresentation( + verifiablePresentation: W3cVerifiablePresentation | SdJwtVc +): SphereonW3cVerifiablePresentation | SphereonCompactSdJwtVc { + // encoded sd-jwt or jwt + if (typeof verifiablePresentation === 'string') { + return verifiablePresentation + } else if (verifiablePresentation instanceof W3cJsonLdVerifiablePresentation) { + return JsonTransformer.toJSON(verifiablePresentation) as SphereonW3cVerifiablePresentation + } else if (verifiablePresentation instanceof W3cJwtVerifiablePresentation) { + return verifiablePresentation.serializedJwt + } else { + return verifiablePresentation.compact + } +} + export function getVerifiablePresentationFromSphereonWrapped( wrappedVerifiablePresentation: WrappedVerifiablePresentation ): W3cVerifiablePresentation | SdJwtVc { diff --git a/packages/openid4vc/src/shared/utils.ts b/packages/openid4vc/src/shared/utils.ts index 5351a7e48a..922b5632e9 100644 --- a/packages/openid4vc/src/shared/utils.ts +++ b/packages/openid4vc/src/shared/utils.ts @@ -1,5 +1,6 @@ -import type { AgentContext, VerificationMethod, JwaSignatureAlgorithm, Key } from '@aries-framework/core' -import type { DIDDocument, SigningAlgo } from '@sphereon/did-auth-siop' +import type { OpenId4VcJwtIssuer } from './models' +import type { AgentContext, JwaSignatureAlgorithm, Key } from '@aries-framework/core' +import type { DIDDocument, SigningAlgo, SuppliedSignature } from '@sphereon/did-auth-siop' import { AriesFrameworkError, @@ -33,27 +34,33 @@ export function getSupportedJwaSignatureAlgorithms(agentContext: AgentContext): return supportedJwaSignatureAlgorithms } -export function getSupportedDidMethods(agentContext: AgentContext) { - const didsApi = agentContext.dependencyManager.resolve(DidsApi) - const supportedDidMethods: Set = new Set() - - for (const resolver of didsApi.config.resolvers) { - resolver.supportedMethods.forEach((method) => supportedDidMethods.add(method)) - } - - return Array.from(supportedDidMethods) -} - -export async function getSuppliedSignatureFromVerificationMethod( +export async function getSphereonSuppliedSignatureFromJwtIssuer( agentContext: AgentContext, - verificationMethod: VerificationMethod -) { - // get the key from the verification method and use the first supported signature algorithm - const key = getKeyFromVerificationMethod(verificationMethod) - const alg = getJwkClassFromKeyType(key.keyType)?.supportedSignatureAlgorithms[0] - if (!alg) throw new AriesFrameworkError(`No supported signature algorithms for key type: ${key.keyType}`) + jwtIssuer: OpenId4VcJwtIssuer +): Promise { + let key: Key + let alg: string + let kid: string | undefined + let did: string | undefined + + if (jwtIssuer.method === 'did') { + const didsApi = agentContext.dependencyManager.resolve(DidsApi) + const didDocument = await didsApi.resolveDidDocument(jwtIssuer.didUrl) + const verificationMethod = didDocument.dereferenceKey(jwtIssuer.didUrl, ['authentication']) + + // get the key from the verification method and use the first supported signature algorithm + key = getKeyFromVerificationMethod(verificationMethod) + const _alg = getJwkClassFromKeyType(key.keyType)?.supportedSignatureAlgorithms[0] + if (!_alg) throw new AriesFrameworkError(`No supported signature algorithms for key type: ${key.keyType}`) + + alg = _alg + kid = verificationMethod.id + did = verificationMethod.controller + } else { + throw new AriesFrameworkError("Unsupported jwt issuer method. Only 'did' is supported.") + } - const suppliedSignature = { + return { signature: async (data: string | Uint8Array) => { if (typeof data !== 'string') throw new AriesFrameworkError("Expected string but received 'Uint8Array'") const signedData = await agentContext.wallet.sign({ @@ -65,14 +72,12 @@ export async function getSuppliedSignatureFromVerificationMethod( return signature }, alg: alg as unknown as SigningAlgo, - did: verificationMethod.controller, - kid: verificationMethod.id, + did, + kid, } - - return suppliedSignature } -export function getResolver(agentContext: AgentContext) { +export function getSphereonDidResolver(agentContext: AgentContext) { return { resolve: async (didUrl: string) => { const didsApi = agentContext.dependencyManager.resolve(DidsApi) @@ -86,7 +91,7 @@ export function getResolver(agentContext: AgentContext) { } } -export const getProofTypeFromKey = (agentContext: AgentContext, key: Key) => { +export function getProofTypeFromKey(agentContext: AgentContext, key: Key) { const signatureSuiteRegistry = agentContext.dependencyManager.resolve(SignatureSuiteRegistry) const supportedSignatureSuites = signatureSuiteRegistry.getByKeyType(key.keyType) diff --git a/packages/openid4vc/tests/utilsVp.ts b/packages/openid4vc/tests/utilsVp.ts index 67956b066d..eb9713f0f3 100644 --- a/packages/openid4vc/tests/utilsVp.ts +++ b/packages/openid4vc/tests/utilsVp.ts @@ -1,6 +1,4 @@ -import type { HolderMetadata } from '../src' -import type { AgentContext, VerificationMethod } from '@aries-framework/core' -import type { PresentationDefinitionV2 } from '@sphereon/pex-models' +import type { AgentContext, DifPresentationExchangeDefinitionV2, VerificationMethod } from '@aries-framework/core' import { getKeyFromVerificationMethod, @@ -11,12 +9,7 @@ import { ClaimFormat, CREDENTIALS_CONTEXT_V1_URL, } from '@aries-framework/core' -import { SigningAlgo } from '@sphereon/did-auth-siop' -import { - openidStaticOpConfiguration, - siopv2StaticOpConfiguration, -} from '../src/openid4vc-verifier/staticOpConfiguration' import { getProofTypeFromKey } from '../src/shared/utils' export const waltPortalOpenBadgeJwt = @@ -56,7 +49,7 @@ export const getOpenBadgeCredentialLdpVc = async ( return signedLdpVc } -export const openBadgeCredentialPresentationDefinitionLdpVc: PresentationDefinitionV2 = { +export const openBadgeCredentialPresentationDefinitionLdpVc: DifPresentationExchangeDefinitionV2 = { id: 'OpenBadgeCredential', input_descriptors: [ { @@ -71,7 +64,7 @@ export const openBadgeCredentialPresentationDefinitionLdpVc: PresentationDefinit ], } -export const universityDegreePresentationDefinition: PresentationDefinitionV2 = { +export const universityDegreePresentationDefinition: DifPresentationExchangeDefinitionV2 = { id: 'UniversityDegreeCredential', input_descriptors: [ { @@ -86,7 +79,7 @@ export const universityDegreePresentationDefinition: PresentationDefinitionV2 = ], } -export const openBadgePresentationDefinition: PresentationDefinitionV2 = { +export const openBadgePresentationDefinition: DifPresentationExchangeDefinitionV2 = { id: 'OpenBadgeCredential', input_descriptors: [ { @@ -102,28 +95,14 @@ export const openBadgePresentationDefinition: PresentationDefinitionV2 = { } export const combinePresentationDefinitions = ( - presentationDefinitions: PresentationDefinitionV2[] -): PresentationDefinitionV2 => { + presentationDefinitions: DifPresentationExchangeDefinitionV2[] +): DifPresentationExchangeDefinitionV2 => { return { id: 'Combined', input_descriptors: presentationDefinitions.flatMap((p) => p.input_descriptors), } } -export const staticOpOpenIdConfigEdDSA = { - ...openidStaticOpConfiguration, - idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA], - requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA], - vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] }, jwt_vp: { alg: [SigningAlgo.EDDSA] } }, -} satisfies HolderMetadata - -export const staticSiopConfigEDDSA = { - ...siopv2StaticOpConfiguration, - idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA], - requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA], - vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] }, jwt_vp: { alg: [SigningAlgo.EDDSA] } }, -} satisfies HolderMetadata - // eslint-disable-next-line @typescript-eslint/no-explicit-any export function waitForMockFunction(mockFn: jest.Mock) { return new Promise((resolve, reject) => { diff --git a/yarn.lock b/yarn.lock index 89102788a1..0460bc2a93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2518,23 +2518,6 @@ resolved "https://registry.yarnpkg.com/@react-native/polyfills/-/polyfills-2.0.0.tgz#4c40b74655c83982c8cf47530ee7dc13d957b6aa" integrity sha512-K0aGNn1TjalKj+65D7ycc1//H9roAQ51GJVk5ZJQFb2teECGmzd86bYDC0aYdbRf7gtovescq4Zt6FR0tgXiHQ== -"@sd-jwt/core@0.1.2-alpha.0": - version "0.1.2-alpha.0" - resolved "https://registry.yarnpkg.com/@sd-jwt/core/-/core-0.1.2-alpha.0.tgz#a1b6ed2c7efc6d71d8fcd063b6624cf77c1eb21f" - integrity sha512-x4MVXar6WmPauZDRJ3aHwaY8o/bHzN77Ts7o43JKuuqIBFjPgAcSlRtd/Xk1rWhazFai4MCIwJDSQ1OQRJtNug== - dependencies: - buffer "*" - -"@sd-jwt/core@^0.1.2-alpha.2": - version "0.1.2-alpha.9" - resolved "https://registry.yarnpkg.com/@sd-jwt/core/-/core-0.1.2-alpha.9.tgz#ddd1c74db273e43fb41a43a25e33367e3d5afc30" - integrity sha512-yMabWCD1ImxFmgaqMg95TOWCiJpjXrVFYWBtCeHUk+O0SuGScB30KVcDmEo6/8vm5CyAAJg2TT156MJoDkqTDA== - dependencies: - "@sd-jwt/decode" "0.1.2-alpha.9" - "@sd-jwt/present" "0.1.2-alpha.9" - "@sd-jwt/types" "0.1.2-alpha.9" - "@sd-jwt/utils" "0.1.2-alpha.9" - "@sd-jwt/core@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@sd-jwt/core/-/core-0.2.0.tgz#e06736ff4920570660fce4e040fe40e900c7fcfa" @@ -2545,14 +2528,6 @@ "@sd-jwt/types" "0.2.0" "@sd-jwt/utils" "0.2.0" -"@sd-jwt/decode@0.1.2-alpha.9": - version "0.1.2-alpha.9" - resolved "https://registry.yarnpkg.com/@sd-jwt/decode/-/decode-0.1.2-alpha.9.tgz#02bb88725ba8e3ca0957624ef3eee7d2e3dc2ef9" - integrity sha512-3Hx5yd1b9gDC0wK7ZkNVzKevyvdGGkmV+mK7/LBUIR+q5SLZlwOmIHz80EM+8Eg0WFAnAmRgWKjn7jWWRQO5dw== - dependencies: - "@sd-jwt/types" "0.1.2-alpha.9" - "@sd-jwt/utils" "0.1.2-alpha.9" - "@sd-jwt/decode@0.2.0", "@sd-jwt/decode@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@sd-jwt/decode/-/decode-0.2.0.tgz#44211418fd0884a160f8223feedfe04ae52398c4" @@ -2561,14 +2536,6 @@ "@sd-jwt/types" "0.2.0" "@sd-jwt/utils" "0.2.0" -"@sd-jwt/present@0.1.2-alpha.9": - version "0.1.2-alpha.9" - resolved "https://registry.yarnpkg.com/@sd-jwt/present/-/present-0.1.2-alpha.9.tgz#f0577dcc66dc08e6bc91faf108565e0fc40d2383" - integrity sha512-LR7uIoC4As2EmGke+lCv2GifG2Xmr4iEFacx30GJW1n35T7vRBDpldIuoMNqHmsR+z3eHx/TWtjrXsh7lGcFtw== - dependencies: - "@sd-jwt/types" "0.1.2-alpha.9" - "@sd-jwt/utils" "0.1.2-alpha.9" - "@sd-jwt/present@0.2.0", "@sd-jwt/present@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@sd-jwt/present/-/present-0.2.0.tgz#01ecbd09dd21287be892b36d754a79c8629387f2" @@ -2577,24 +2544,11 @@ "@sd-jwt/types" "0.2.0" "@sd-jwt/utils" "0.2.0" -"@sd-jwt/types@0.1.2-alpha.9": - version "0.1.2-alpha.9" - resolved "https://registry.yarnpkg.com/@sd-jwt/types/-/types-0.1.2-alpha.9.tgz#198899e3a98f9329f35b20fd8af5c1ee37e1e739" - integrity sha512-j7Nf3RhQshkEjf3RQhF5hWMMOPQmzwhXBUhjcOoF5eJNgkpF6R13ryb3GDJHkRomIhkygaWaFEzC+ioRAZ7FzQ== - "@sd-jwt/types@0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@sd-jwt/types/-/types-0.2.0.tgz#3cb50392e1b76ce69453f403c71c937a6e202352" integrity sha512-16WFRcL/maG0/JxN9UCSx07/vJ2SDbGscv9gDLmFLgJzhJcGPer41XfI6aDfVARYP430wHFixChfY/n7qC1L/Q== -"@sd-jwt/utils@0.1.2-alpha.9": - version "0.1.2-alpha.9" - resolved "https://registry.yarnpkg.com/@sd-jwt/utils/-/utils-0.1.2-alpha.9.tgz#efbde280798afb964e829726214167f50e7022a3" - integrity sha512-oPNWO/XDUkJxdEyOZvmLoqCo0uwiu5Xk0wGkmpwB9KtzeaioVW3JziFUswEczE9RED4+dOWtQwbSpEcy1DEWQw== - dependencies: - "@sd-jwt/types" "0.1.2-alpha.9" - buffer "*" - "@sd-jwt/utils@0.2.0", "@sd-jwt/utils@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@sd-jwt/utils/-/utils-0.2.0.tgz#ef52b744116e874f72ec01978f0631ad5a131eb7" @@ -2654,16 +2608,16 @@ resolved "https://registry.yarnpkg.com/@sovpro/delimited-stream/-/delimited-stream-1.1.0.tgz#4334bba7ee241036e580fdd99c019377630d26b4" integrity sha512-kQpk267uxB19X3X2T1mvNMjyvIEonpNSHrMlK5ZaBU6aZxw7wPbpgKJOjHN3+/GPVpXgAV9soVT2oyHpLkLtyw== -"@sphereon/did-auth-siop@0.6.0-unstable.0": - version "0.6.0-unstable.0" - resolved "https://registry.yarnpkg.com/@sphereon/did-auth-siop/-/did-auth-siop-0.6.0-unstable.0.tgz#1e7c8bafec4e36ec12eec7adfe48d21d0009af4a" - integrity sha512-k5z1sGOv+B8oz0H5zWHZE+1myDpM4RAB7UcS7Z3kNMgP27mK4VpvUFIYKJ3CW/3+QzxgDA5V7d/S6dqsjgmoKQ== +"@sphereon/did-auth-siop@0.6.0-unstable.3": + version "0.6.0-unstable.3" + resolved "https://registry.yarnpkg.com/@sphereon/did-auth-siop/-/did-auth-siop-0.6.0-unstable.3.tgz#705dfd17210846b382f3116a92d9d2e7242b93e3" + integrity sha512-0d2A3EPsywkHw5zfR3JWu0sjy3FACtpAlnWabol/5C8/C1Ys1hCk+X995aADqs8DRtdVFX8TFJkCMshp7pLyEg== dependencies: "@astronautlabs/jsonpath" "^1.1.2" "@sphereon/did-uni-client" "^0.6.1" - "@sphereon/pex" "^3.0.0" + "@sphereon/pex" "^3.0.1" "@sphereon/pex-models" "^2.1.5" - "@sphereon/ssi-types" "^0.18.0" + "@sphereon/ssi-types" "0.18.1" "@sphereon/wellknown-dids-client" "^0.1.3" cross-fetch "^4.0.0" did-jwt "6.11.6" @@ -2684,32 +2638,32 @@ cross-fetch "^4.0.0" did-resolver "^4.1.0" -"@sphereon/oid4vci-client@0.8.2-next.34": - version "0.8.2-next.34" - resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-client/-/oid4vci-client-0.8.2-next.34.tgz#6b24b669f71ca6575bacf5305aeb70a1f22a2e57" - integrity sha512-p/iTvXz9XckNDgP2AmtZPWmvpNUCAas1Pe9XQIZLL/TXYNtgO32P+woY6FAeNrHAAfvyVu/DCuosps31bB/7lA== +"@sphereon/oid4vci-client@0.8.2-next.46": + version "0.8.2-next.46" + resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-client/-/oid4vci-client-0.8.2-next.46.tgz#0f53dc607a0ee17cf0bedb4e7ca91fe525a4c44e" + integrity sha512-oYY5RbFEpyYMU+EHriGOb/noFpFWhpgimr6drdAI7l5hMIQTs3iz8kUk9CSCJEOYq0n9VtWzd9jE3qDVjMgepA== dependencies: - "@sphereon/oid4vci-common" "0.8.2-next.34+b3f0cf1" - "@sphereon/ssi-types" "^0.18.0" + "@sphereon/oid4vci-common" "0.8.2-next.46+e3c1601" + "@sphereon/ssi-types" "^0.18.1" cross-fetch "^3.1.8" debug "^4.3.4" -"@sphereon/oid4vci-common@0.8.2-next.34", "@sphereon/oid4vci-common@0.8.2-next.34+b3f0cf1": - version "0.8.2-next.34" - resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-common/-/oid4vci-common-0.8.2-next.34.tgz#821186b7153b4dadbeddd2d7ca0d65e5a8acac84" - integrity sha512-jkuusNR0aa8itQwYgQMo4Jzy0ViyUFvUK4BS9GiQmemD9fBbqAmvXNRXCHz2KI9H1wbhw45CF+BKVNYB1REdHA== +"@sphereon/oid4vci-common@0.8.2-next.46", "@sphereon/oid4vci-common@0.8.2-next.46+e3c1601": + version "0.8.2-next.46" + resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-common/-/oid4vci-common-0.8.2-next.46.tgz#5def7c2aa68b19a7f52691668580755573db28e1" + integrity sha512-mt21K/bukcwdqB3kfKGFj3597rO3WnxW7Dietd0YE87C8yt7WyapXdogP7p18GJ40zu6+OealIeNnEMxCBQPXA== dependencies: - "@sphereon/ssi-types" "^0.18.0" + "@sphereon/ssi-types" "^0.18.1" cross-fetch "^3.1.8" jwt-decode "^3.1.2" -"@sphereon/oid4vci-issuer@0.8.2-next.34": - version "0.8.2-next.34" - resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-issuer/-/oid4vci-issuer-0.8.2-next.34.tgz#b04cbb16b166d2081dcfd730f7159cc716af5179" - integrity sha512-+p+U9AwuYLIZvzuheo0p1iWeeFLSAL3U+vy9boF+uBRhhBwv05oJMzKCrPjPFPbNCaCGffUC/KYRJkl4QR/VsA== +"@sphereon/oid4vci-issuer@0.8.2-next.46": + version "0.8.2-next.46" + resolved "https://registry.yarnpkg.com/@sphereon/oid4vci-issuer/-/oid4vci-issuer-0.8.2-next.46.tgz#3886b5e1b9203de8b6d7c5b435562f888177a87b" + integrity sha512-9/VG9QulFEDpNvEe8X7YCcc2FwUDpR2e7geWdWY9SyOexYtjxTcoyfHb9bPgIg5TuFbA1nADTD804935suhKtw== dependencies: - "@sphereon/oid4vci-common" "0.8.2-next.34+b3f0cf1" - "@sphereon/ssi-types" "^0.18.0" + "@sphereon/oid4vci-common" "0.8.2-next.46+e3c1601" + "@sphereon/ssi-types" "^0.18.1" uuid "^9.0.0" "@sphereon/pex-models@^2.1.5": @@ -2717,21 +2671,6 @@ resolved "https://registry.yarnpkg.com/@sphereon/pex-models/-/pex-models-2.1.5.tgz#ba4474a3783081392b72403c4c8ee6da3d2e5585" integrity sha512-7THexvdYUK/Dh8olBB46ErT9q/RnecnMdb5r2iwZ6be0Dt4vQLAUN7QU80H0HZBok4jRTb8ydt12x0raBSTHOg== -"@sphereon/pex@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-3.0.0.tgz#97ac049b279a50115f02c85185d1f348af9d6b91" - integrity sha512-viD3Enwt/Wf/RoTBbZXttNAcE0Scyb+UufwkNaLU4sD2nNkvi3VuJ4dSMMFGm3QlIgBWxSm1N4DnSm+LRMZidQ== - dependencies: - "@astronautlabs/jsonpath" "^1.1.2" - "@sd-jwt/core" "^0.1.2-alpha.2" - "@sphereon/pex-models" "^2.1.5" - "@sphereon/ssi-types" "0.18.0" - ajv "^8.12.0" - ajv-formats "^2.1.1" - jwt-decode "^3.1.2" - nanoid "^3.3.7" - string.prototype.matchall "^4.0.10" - "@sphereon/pex@^3.0.1": version "3.0.1" resolved "https://registry.yarnpkg.com/@sphereon/pex/-/pex-3.0.1.tgz#e7d9d36c7c921ab97190a735c67e0a2632432e3b" @@ -2749,14 +2688,6 @@ nanoid "^3.3.7" string.prototype.matchall "^4.0.10" -"@sphereon/ssi-types@0.18.0", "@sphereon/ssi-types@^0.18.0": - version "0.18.0" - resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.18.0.tgz#b4f3a4a9e4e5b28719ce729b679d71d9d220cc26" - integrity sha512-D2n42NAhHCwpL4K7BqQXO9dYQ8n3st/1eJQrLqokJ18B9r2gury3km4cp+ZdiIxfefUaP9RBCeuWaiRUvjZ94w== - dependencies: - "@sd-jwt/core" "0.1.2-alpha.0" - jwt-decode "^3.1.2" - "@sphereon/ssi-types@0.18.1", "@sphereon/ssi-types@^0.18.1": version "0.18.1" resolved "https://registry.yarnpkg.com/@sphereon/ssi-types/-/ssi-types-0.18.1.tgz#c00e4939149f4e441fae56af860735886a4c33a5" From f2ea10743e6475e39f43928cbc13681bcd55b8cc Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sun, 28 Jan 2024 15:15:40 +0700 Subject: [PATCH 112/115] some nice fixes Signed-off-by: Timo Glastra --- packages/core/package.json | 3 +- .../DifPresentationExchangeService.ts | 12 +- .../utils/credentialSelection.ts | 19 +- .../utils/transform.ts | 2 +- .../core/src/modules/sd-jwt-vc/SdJwtVcApi.ts | 4 +- .../src/modules/sd-jwt-vc/SdJwtVcService.ts | 66 +-- .../__tests__/SdJwtVcService.test.ts | 16 + .../sd-jwt-vc/__tests__/sdJwtVc.e2e.test.ts | 9 +- .../__tests__/SdJwtVcRecord.test.ts | 26 +- .../OpenId4vcSiopHolderService.ts | 4 + .../openid4vc-issuer/OpenId4VcIssuerApi.ts | 5 +- .../__tests__/openid4vc-issuer.e2e.test.ts | 22 +- .../OpenId4VcSiopVerifierService.ts | 40 +- .../OpenId4VcSiopVerifierServiceOptions.ts | 2 + packages/openid4vc/src/shared/models/index.ts | 2 + .../openid4vc/tests/openid4vc.e2e.test.ts | 387 ++++++++++++++---- yarn.lock | 2 +- 17 files changed, 430 insertions(+), 191 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index b97acf99f0..dbc9021b1f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -23,11 +23,12 @@ "prepublishOnly": "yarn run build" }, "dependencies": { - "@sd-jwt/core": "^0.2.0", "@digitalcredentials/jsonld": "^5.2.1", "@digitalcredentials/jsonld-signatures": "^9.3.1", "@digitalcredentials/vc": "^1.1.2", "@multiformats/base-x": "^4.0.1", + "@sd-jwt/core": "^0.2.0", + "@sd-jwt/decode": "^0.2.0", "@sphereon/pex": "^3.0.1", "@sphereon/pex-models": "^2.1.5", "@sphereon/ssi-types": "^0.18.1", diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts index 3708a941fc..608304c22d 100644 --- a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts @@ -26,7 +26,7 @@ import { injectable } from 'tsyringe' import { getJwkFromKey } from '../../crypto' import { AriesFrameworkError } from '../../error' -import { Hasher, JsonTransformer, TypedArrayEncoder } from '../../utils' +import { Hasher, JsonTransformer } from '../../utils' import { DidsApi, getKeyFromVerificationMethod } from '../dids' import { SdJwtVcApi } from '../sd-jwt-vc' import { @@ -214,12 +214,10 @@ export class DifPresentationExchangeService { } return { - verifiablePresentations: await Promise.all( - verifiablePresentationResultsWithFormat.map((resultWithFormat) => - getVerifiablePresentationFromEncoded( - agentContext, - resultWithFormat.verifiablePresentationResult.verifiablePresentation - ) + verifiablePresentations: verifiablePresentationResultsWithFormat.map((resultWithFormat) => + getVerifiablePresentationFromEncoded( + agentContext, + resultWithFormat.verifiablePresentationResult.verifiablePresentation ) ), presentationSubmission, diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts index ce0975594f..c1ef770b36 100644 --- a/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts +++ b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts @@ -29,7 +29,24 @@ export async function getCredentialsForRequest( ...selectResultsRaw, // Map the encoded credential to their respective w3c credential record verifiableCredential: selectResultsRaw.verifiableCredential?.map((selectedEncoded) => { - const credentialRecordIndex = encodedCredentials.findIndex((encoded) => deepEquality(selectedEncoded, encoded)) + const credentialRecordIndex = encodedCredentials.findIndex((encoded) => { + if ( + typeof selectedEncoded === 'string' && + selectedEncoded.includes('~') && + typeof encoded === 'string' && + encoded.includes('~') + ) { + // FIXME: pex applies SD-JWT, so we actually can't match the record anymore :( + // We take the first part of the sd-jwt, as that will never change, and should + // be unique on it's own + const [encodedJwt] = encoded.split('~') + const [selectedEncodedJwt] = selectedEncoded.split('~') + + return encodedJwt === selectedEncodedJwt + } else { + return deepEquality(selectedEncoded, encoded) + } + }) if (credentialRecordIndex === -1) { throw new DifPresentationExchangeError('Unable to find credential in credential records.') diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts b/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts index 3c368ab979..636b106d51 100644 --- a/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts +++ b/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts @@ -37,7 +37,7 @@ export function getSphereonOriginalVerifiablePresentation( } // TODO: we might want to move this to some generic vc transformation util -export async function getVerifiablePresentationFromEncoded( +export function getVerifiablePresentationFromEncoded( agentContext: AgentContext, encodedVerifiablePresentation: string | W3cJsonPresentation | SphereonW3CVerifiablePresentation ) { diff --git a/packages/core/src/modules/sd-jwt-vc/SdJwtVcApi.ts b/packages/core/src/modules/sd-jwt-vc/SdJwtVcApi.ts index 784a3b26e8..17dbd81273 100644 --- a/packages/core/src/modules/sd-jwt-vc/SdJwtVcApi.ts +++ b/packages/core/src/modules/sd-jwt-vc/SdJwtVcApi.ts @@ -57,8 +57,8 @@ export class SdJwtVcApi { /** * Get and validate a sd-jwt-vc from a serialized JWT. */ - public async fromCompact
(sdJwtVcCompact: string) { - return await this.sdJwtVcService.fromCompact(sdJwtVcCompact) + public fromCompact
(sdJwtVcCompact: string) { + return this.sdJwtVcService.fromCompact(sdJwtVcCompact) } public async store(compactSdJwtVc: string) { diff --git a/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts b/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts index 21f05d5e7f..95281d2ba2 100644 --- a/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts +++ b/packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts @@ -13,6 +13,7 @@ import type { Query } from '../../storage/StorageService' import type { Signer, SdJwtVcVerificationResult, Verifier, HasherAndAlgorithm, DisclosureItem } from '@sd-jwt/core' import { KeyBinding, SdJwtVc as _SdJwtVc, HasherAlgorithm } from '@sd-jwt/core' +import { decodeSdJwtVc } from '@sd-jwt/decode' import { injectable } from 'tsyringe' import { Jwk, getJwkFromJson, getJwkFromKey } from '../../crypto' @@ -93,22 +94,17 @@ export class SdJwtVcService { } satisfies SdJwtVc } - public async fromCompact< - Header extends SdJwtVcHeader = SdJwtVcHeader, - Payload extends SdJwtVcPayload = SdJwtVcPayload - >(compactSdJwtVc: string): Promise> { - const sdJwtVc = _SdJwtVc.fromCompact(compactSdJwtVc).withHasher(this.hasher) - - if (!sdJwtVc.signature) { - throw new SdJwtVcError('A signature must be included for an sd-jwt-vc') - } + public fromCompact
( + compactSdJwtVc: string + ): SdJwtVc { + // NOTE: we use decodeSdJwtVc so we can make this method sync + const { decodedPayload, header, signedPayload } = decodeSdJwtVc(compactSdJwtVc, Hasher.hash) return { compact: compactSdJwtVc, - header: sdJwtVc.header, - payload: sdJwtVc.payload, - - prettyClaims: await sdJwtVc.getPrettyClaims(), + header: header as Header, + payload: signedPayload as Payload, + prettyClaims: decodedPayload as Payload, } } @@ -161,11 +157,16 @@ export class SdJwtVcService { const issuer = await this.extractKeyFromIssuer(agentContext, this.parseIssuerFromCredential(sdJwtVc)) const holder = await this.extractKeyFromHolderBinding(agentContext, this.parseHolderBindingFromCredential(sdJwtVc)) - // FIXME: sdJwtVc library must support passing a custom jwk resolver based on the cnf claim - // or passing in the resolved key already. Currently the implementation assumes the cnf is always - // a jwk. But this won't work if we want to bind the cnf to a did + // FIXME: we currently pass in the required keys in the verification method and based on the header.typ we + // check if we need to use the issuer or holder key. Once better support in sd-jwt lib is available we can + // update this. + // See https://github.com/berendsliedrecht/sd-jwt-ts/pull/34 + // See https://github.com/berendsliedrecht/sd-jwt-ts/issues/15 const verificationResult = await sdJwtVc.verify( - this.verifier(agentContext, issuer.key), + this.verifier(agentContext, { + issuer: issuer.key, + holder: holder.key, + }), requiredClaimKeys, holder.cnf ) @@ -267,28 +268,33 @@ export class SdJwtVcService { /** * @todo validate the JWT header (alg) - * FIXME: also support kid (did) for cnf claim */ private verifier
( agentContext: AgentContext, - signerKey: Key + verificationKeys: { + issuer: Key + holder: Key + } ): Verifier
{ - return async ({ message, signature, publicKeyJwk }) => { - let key = signerKey - - if (publicKeyJwk) { - if (!('kty' in publicKeyJwk)) { - throw new SdJwtVcError( - 'Key type (kty) claim could not be found in the JWK of the confirmation (cnf) claim. Only JWK is supported right now' - ) - } + return async ({ message, signature, publicKeyJwk, header }) => { + const keyFromPublicKeyJwk = publicKeyJwk ? getJwkFromJson(publicKeyJwk as JwkJson).key : undefined + + let key: Key + if (header.typ === 'kb+jwt') { + key = verificationKeys.holder + } else if (header.typ === 'vc+sd-jwt') { + key = verificationKeys.issuer + } else { + throw new SdJwtVcError(`Unsupported JWT type '${header.typ}'`) + } - key = getJwkFromJson(publicKeyJwk as JwkJson).key + if (keyFromPublicKeyJwk && key.fingerprint !== keyFromPublicKeyJwk.fingerprint) { + throw new SdJwtVcError('The key used to verify the signature does not match the expected key') } return await agentContext.wallet.verify({ signature: Buffer.from(signature), - key: key, + key, data: TypedArrayEncoder.fromString(message), }) } diff --git a/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcService.test.ts b/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcService.test.ts index 08b58a850a..c49afc452e 100644 --- a/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcService.test.ts +++ b/packages/core/src/modules/sd-jwt-vc/__tests__/SdJwtVcService.test.ts @@ -568,5 +568,21 @@ describe('SdJwtVcService', () => { isKeyBindingValid: true, }) }) + + test('Verify did holder-bound sd-jwt-vc with disclosures and kb-jwt', async () => { + const verificationResult = await sdJwtVcService.verify( + agent.context, + { + compactSdJwtVc: + 'eyJhbGciOiJFZERTQSIsInR5cCI6InZjK3NkLWp3dCIsImtpZCI6IiN6Nk1rcnpRUEJyNHB5cUM3NzZLS3RyejEzU2NoTTVlUFBic3N1UHVRWmI1dDR1S1EifQ.eyJ2Y3QiOiJPcGVuQmFkZ2VDcmVkZW50aWFsIiwiZGVncmVlIjoiYmFjaGVsb3IiLCJjbmYiOnsia2lkIjoiZGlkOmtleTp6Nk1rcEdSNGdzNFJjM1pwaDR2ajh3Um5qbkF4Z0FQU3hjUjhNQVZLdXRXc3BRemMjejZNa3BHUjRnczRSYzNacGg0dmo4d1Juam5BeGdBUFN4Y1I4TUFWS3V0V3NwUXpjIn0sImlzcyI6ImRpZDprZXk6ejZNa3J6UVBCcjRweXFDNzc2S0t0cnoxM1NjaE01ZVBQYnNzdVB1UVpiNXQ0dUtRIiwiaWF0IjoxNzA2MjY0ODQwLCJfc2RfYWxnIjoic2hhLTI1NiIsIl9zZCI6WyJTSm81ME0xX3NUQWRPSjFObF82ekJzZWg3Ynd4czhhbDhleVotQl9nTXZFIiwiYTJjT2xWOXY4TUlWNTRvMVFrODdBMDRNZ0c3Q0hiaFZUN1lkb00zUnM4RSJdfQ.PrZtmLFPr8tBY0FKsv2yHJeqzds8m0Rlrof-Z36o7ksNvON3ZSrKHOD8fFDJaQ8oFJcZAnjpUS6pY9kwAgU1Ag~WyI5Mjg3NDM3NDQyMTg0ODk1NTU3OTA1NTkiLCJ1bml2ZXJzaXR5IiwiaW5uc2JydWNrIl0~eyJhbGciOiJFZERTQSIsInR5cCI6ImtiK2p3dCJ9.eyJpYXQiOjE3MDYyNjQ4NDAsIm5vbmNlIjoiODExNzMxNDIwNTMxODQ3NzAwNjM2ODUiLCJhdWQiOiJkaWQ6a2V5Ono2TWt0aVFRRXFtMnlhcFhCRHQxV0VWQjNkcWd2eXppOTZGdUZBTlltcmdUcktWOSIsIl9zZF9oYXNoIjoiSVd0cTEtOGUtLU9wWWpXa3Z1RTZrRjlaa2h5aDVfV3lOYXItaWtVT0FscyJ9.cJNnYH16lHn0PsF9tOQPofpONGoY19bQB5k6Ezux9TvQWS_Opnd-3m_fO9yKu8S0pmjyG2mu3Uzn1pUNqhL9AQ', + keyBinding: { + audience: 'did:key:z6MktiQQEqm2yapXBDt1WEVB3dqgvyzi96FuFANYmrgTrKV9', + nonce: '81173142053184770063685', + }, + } + ) + + expect(verificationResult.verification.isValid).toBe(true) + }) }) }) diff --git a/packages/core/src/modules/sd-jwt-vc/__tests__/sdJwtVc.e2e.test.ts b/packages/core/src/modules/sd-jwt-vc/__tests__/sdJwtVc.e2e.test.ts index 72f55af7ad..aeffd08876 100644 --- a/packages/core/src/modules/sd-jwt-vc/__tests__/sdJwtVc.e2e.test.ts +++ b/packages/core/src/modules/sd-jwt-vc/__tests__/sdJwtVc.e2e.test.ts @@ -82,13 +82,6 @@ describe('sd-jwt-vc end to end test', () => { const { compact, header, payload } = await issuer.sdJwtVc.sign({ payload: credential, - // FIXME: sd-jwt library does not support did binding for holder yet - // issuance is fine, but in verification of KB the jwk will be extracted - // from the cnf claim which will be undefined. - // holder: { - // method: 'did', - // didUrl: holderDidUrl, - // }, holder: { method: 'jwk', jwk: getJwkFromKey(holderKey), @@ -115,7 +108,7 @@ describe('sd-jwt-vc end to end test', () => { type Header = typeof header // parse SD-JWT - const sdJwtVc = await holder.sdJwtVc.fromCompact(compact) + const sdJwtVc = holder.sdJwtVc.fromCompact(compact) expect(sdJwtVc).toEqual({ compact: expect.any(String), header: { diff --git a/packages/core/src/modules/sd-jwt-vc/repository/__tests__/SdJwtVcRecord.test.ts b/packages/core/src/modules/sd-jwt-vc/repository/__tests__/SdJwtVcRecord.test.ts index e288aab5cd..e08cf28d34 100644 --- a/packages/core/src/modules/sd-jwt-vc/repository/__tests__/SdJwtVcRecord.test.ts +++ b/packages/core/src/modules/sd-jwt-vc/repository/__tests__/SdJwtVcRecord.test.ts @@ -21,16 +21,9 @@ describe('SdJwtVcRecord', () => { expect(sdJwtVcRecord.createdAt).toBe(createdAt) expect(sdJwtVcRecord.getTags()).toEqual({ some: 'tag', - disclosureKeys: [ - 'is_over_65', - 'is_over_21', - 'is_over_18', - 'birthdate', - 'email', - 'region', - 'country', - 'given_name', - ], + alg: 'EdDSA', + sdAlg: 'sha-256', + vct: 'IdentityCredential', }) expect(sdJwtVcRecord.compactSdJwtVc).toEqual(compactSdJwtVc) }) @@ -66,16 +59,9 @@ describe('SdJwtVcRecord', () => { expect(instance.createdAt.getTime()).toBe(createdAt.getTime()) expect(instance.getTags()).toEqual({ some: 'tag', - disclosureKeys: [ - 'is_over_65', - 'is_over_21', - 'is_over_18', - 'birthdate', - 'email', - 'region', - 'country', - 'given_name', - ], + alg: 'EdDSA', + sdAlg: 'sha-256', + vct: 'IdentityCredential', }) expect(instance.compactSdJwtVc).toBe(compactSdJwtVc) }) diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts index 29d18f31f6..eec4364477 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts @@ -7,6 +7,7 @@ import type { AgentContext, SdJwtVc, W3cVerifiablePresentation } from '@aries-fr import type { VerifiedAuthorizationRequest, PresentationExchangeResponseOpts } from '@sphereon/did-auth-siop' import { + Hasher, W3cJwtVerifiablePresentation, parseDid, AriesFrameworkError, @@ -15,6 +16,7 @@ import { W3cJsonLdVerifiablePresentation, asArray, DifPresentationExchangeService, + DifPresentationExchangeSubmissionLocation, } from '@aries-framework/core' import { CheckLinkedDomain, @@ -112,6 +114,7 @@ export class OpenId4VcSiopHolderService { presentationDefinition: authorizationRequest.presentationDefinitions[0].definition, challenge: nonce, domain: clientId, + presentationSubmissionLocation: DifPresentationExchangeSubmissionLocation.EXTERNAL, }) presentationExchangeOptions = { @@ -179,6 +182,7 @@ export class OpenId4VcSiopHolderService { .withSupportedVersions([SupportedVersion.SIOPv2_D11, SupportedVersion.SIOPv2_D12_OID4VP_D18]) .withCustomResolver(getSphereonDidResolver(agentContext)) .withCheckLinkedDomain(CheckLinkedDomain.NEVER) + .withHasher(Hasher.hash) if (openIdTokenIssuer) { const suppliedSignature = await getSphereonSuppliedSignatureFromJwtIssuer(agentContext, openIdTokenIssuer) diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts index 7ddd1c2f12..f015eef416 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerApi.ts @@ -1,7 +1,6 @@ import type { OpenId4VciCreateCredentialResponseOptions, OpenId4VciCreateCredentialOfferOptions, - CredentialOffer, } from './OpenId4VcIssuerServiceOptions' import type { OpenId4VcIssuerRecordProps } from './repository' import type { OpenId4VciCredentialOfferPayload } from '../shared' @@ -70,9 +69,7 @@ export class OpenId4VcIssuerApi { * * @returns Object containing the payload of the credential offer and the credential offer request, which can be sent to the wallet. */ - public async createCredentialOffer( - options: OpenId4VciCreateCredentialOfferOptions & { issuerId: string } - ): Promise { + public async createCredentialOffer(options: OpenId4VciCreateCredentialOfferOptions & { issuerId: string }) { const { issuerId, ...rest } = options const issuer = await this.openId4VcIssuerService.getByIssuerId(this.agentContext, issuerId) return await this.openId4VcIssuerService.createCredentialOffer(this.agentContext, { ...rest, issuer }) diff --git a/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.e2e.test.ts b/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.e2e.test.ts index a2767b3b3b..a8be5d1e7a 100644 --- a/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-issuer/__tests__/openid4vc-issuer.e2e.test.ts @@ -297,7 +297,7 @@ describe('OpenId4VcIssuer', () => { }, }) - expect(result.credentialOfferUri).toEqual( + expect(result.credentialOffer).toEqual( `openid-credential-offer://?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FUniversityDegreeCredentialSdJwt%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` ) @@ -353,7 +353,7 @@ describe('OpenId4VcIssuer', () => { }, }) - expect(result.credentialOfferUri).toEqual( + expect(result.credentialOffer).toEqual( `openid-credential-offer://?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` ) @@ -432,7 +432,7 @@ describe('OpenId4VcIssuer', () => { }, }) - expect(result.credentialOfferUri).toEqual( + expect(result.credentialOffer).toEqual( `openid-credential-offer://?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` ) @@ -472,7 +472,7 @@ describe('OpenId4VcIssuer', () => { }, }) - expect(result.credentialOfferUri).toEqual( + expect(result.credentialOffer).toEqual( `openid-credential-offer://?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%2C%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FUniversityDegreeCredentialLd%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` ) @@ -528,7 +528,7 @@ describe('OpenId4VcIssuer', () => { }, }) - expect(result.credentialOfferUri).toEqual( + expect(result.credentialOffer).toEqual( `openid-credential-offer://?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` ) @@ -570,7 +570,7 @@ describe('OpenId4VcIssuer', () => { }, }) - expect(result.credentialOfferUri).toEqual( + expect(result.credentialOffer).toEqual( `openid-credential-offer://?credential_offer=%7B%22grants%22%3A%7B%22authorization_code%22%3A%7B%22issuer_state%22%3A%221234567890%22%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` ) @@ -614,7 +614,7 @@ describe('OpenId4VcIssuer', () => { const hostedCredentialOfferUrl = 'https://openid4vc-issuer.com/credential-offer-uri' - const { credentialOfferUri, credentialOfferPayload } = await issuer.modules.openId4VcIssuer.createCredentialOffer({ + const { credentialOffer, credentialOfferPayload } = await issuer.modules.openId4VcIssuer.createCredentialOffer({ issuerId: openId4VcIssuer.issuerId, offeredCredentials: [openBadgeCredential.id], hostedCredentialOfferUrl, @@ -624,7 +624,7 @@ describe('OpenId4VcIssuer', () => { }, }) - expect(credentialOfferUri).toEqual(`openid-credential-offer://?credential_offer_uri=${hostedCredentialOfferUrl}`) + expect(credentialOffer).toEqual(`openid-credential-offer://?credential_offer_uri=${hostedCredentialOfferUrl}`) const credentialOfferReceivedByUri = await issuer.modules.openId4VcIssuer.getCredentialOfferFromUri( hostedCredentialOfferUrl @@ -636,14 +636,14 @@ describe('OpenId4VcIssuer', () => { it('create credential offer and retrieve it from the uri (authorizationCodeFlow)', async () => { const hostedCredentialOfferUrl = 'https://openid4vc-issuer.com/credential-offer-uri' - const { credentialOfferUri, credentialOfferPayload } = await issuer.modules.openId4VcIssuer.createCredentialOffer({ + const { credentialOffer, credentialOfferPayload } = await issuer.modules.openId4VcIssuer.createCredentialOffer({ offeredCredentials: [openBadgeCredential.id], issuerId: openId4VcIssuer.issuerId, hostedCredentialOfferUrl, authorizationCodeFlowConfig: { issuerState: '1234567890' }, }) - expect(credentialOfferUri).toEqual(`openid-credential-offer://?credential_offer_uri=${hostedCredentialOfferUrl}`) + expect(credentialOffer).toEqual(`openid-credential-offer://?credential_offer_uri=${hostedCredentialOfferUrl}`) const credentialOfferReceivedByUri = await issuer.modules.openId4VcIssuer.getCredentialOfferFromUri( hostedCredentialOfferUrl @@ -670,7 +670,7 @@ describe('OpenId4VcIssuer', () => { }, }) - expect(result.credentialOfferUri).toEqual( + expect(result.credentialOffer).toEqual( `openid-credential-offer://?credential_offer=%7B%22grants%22%3A%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%221234567890%22%2C%22user_pin_required%22%3Afalse%7D%7D%2C%22credentials%22%3A%5B%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FOpenBadgeCredential%22%2C%22https%3A%2F%2Fopenid4vc-issuer.com%2Fcredentials%2FUniversityDegreeCredential%22%5D%2C%22credential_issuer%22%3A%22https%3A%2F%2Fopenid4vc-issuer.com%2F${openId4VcIssuer.issuerId}%22%7D` ) diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts index 81b0134db2..07bafa3c88 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts @@ -84,6 +84,7 @@ export class OpenId4VcSiopVerifierService { return { authorizationRequestUri: authorizationRequestUri.encodedUri, + authorizationRequestPayload: authorizationRequest.payload, } } @@ -240,6 +241,11 @@ export class OpenId4VcSiopVerifierService { throw new AriesFrameworkError("Either 'requestSigner' or 'clientId' must be provided.") } + // FIXME: we now manually remove did:peer, we should probably allow the user to configure this + const supportedDidMethods = agentContext.dependencyManager + .resolve(DidsApi) + .supportedResolverMethods.filter((m) => m !== 'peer') + builder .withRedirectUri(authorizationResponseUrl) .withIssuer(ResponseIss.SELF_ISSUED_V2) @@ -250,6 +256,7 @@ export class OpenId4VcSiopVerifierService { passBy: PassBy.VALUE, idTokenSigningAlgValuesSupported: supportedAlgs as SigningAlgo[], responseTypesSupported: [ResponseType.VP_TOKEN, ResponseType.ID_TOKEN], + subject_syntax_types_supported: supportedDidMethods.map((m) => `did:${m}`), vpFormatsSupported: { jwt_vc: { alg: supportedAlgs, @@ -276,6 +283,7 @@ export class OpenId4VcSiopVerifierService { .withResponseMode(ResponseMode.POST) .withResponseType(presentationDefinition ? [ResponseType.ID_TOKEN, ResponseType.VP_TOKEN] : ResponseType.ID_TOKEN) .withScope('openid') + .withHasher(Hasher.hash) // TODO: support hosting requests within AFJ and passing it by reference .withRequestBy(PassBy.VALUE) .withCheckLinkedDomain(CheckLinkedDomain.NEVER) @@ -286,13 +294,9 @@ export class OpenId4VcSiopVerifierService { .withEventEmitter(this.config.getEventEmitter(agentContext)) if (presentationDefinition) { - builder.withPresentationDefinition({ definition: presentationDefinition }, [ - PropertyTarget.REQUEST_OBJECT, - PropertyTarget.AUTHORIZATION_REQUEST, - ]) + builder.withPresentationDefinition({ definition: presentationDefinition }, [PropertyTarget.REQUEST_OBJECT]) } - const supportedDidMethods = agentContext.dependencyManager.resolve(DidsApi).supportedResolverMethods for (const supportedDidMethod of supportedDidMethods) { builder.addDidMethod(supportedDidMethod) } @@ -310,6 +314,8 @@ export class OpenId4VcSiopVerifierService { if (!encodedPresentation) throw new AriesFrameworkError('Did not receive a presentation for verification.') + let isValid: boolean + // TODO: it might be better here to look at the presentation submission to know // If presentation includes a ~, we assume it's an SD-JWT-VC if (typeof encodedPresentation === 'string' && encodedPresentation.includes('~')) { @@ -323,9 +329,7 @@ export class OpenId4VcSiopVerifierService { }, }) - return { - verified: verificationResult.verification.isValid, - } + isValid = verificationResult.verification.isValid } else if (typeof encodedPresentation === 'string') { const verificationResult = await this.w3cCredentialService.verifyPresentation(agentContext, { presentation: encodedPresentation, @@ -333,9 +337,7 @@ export class OpenId4VcSiopVerifierService { domain: options.audience, }) - return { - verified: verificationResult.isValid, - } + isValid = verificationResult.isValid } else { const verificationResult = await this.w3cCredentialService.verifyPresentation(agentContext, { presentation: JsonTransformer.fromJSON(encodedPresentation, W3cJsonLdVerifiablePresentation), @@ -343,9 +345,19 @@ export class OpenId4VcSiopVerifierService { domain: options.audience, }) - return { - verified: verificationResult.isValid, - } + isValid = verificationResult.isValid + } + + // FIXME: we throw an error here as there's a bug in sphereon library where they + // don't check the returned 'verified' property and only catch errors thrown. + // Once https://github.com/Sphereon-Opensource/SIOP-OID4VP/pull/70 is merged we + // can remove this. + if (!isValid) { + throw new AriesFrameworkError('Presentation verification failed.') + } + + return { + verified: isValid, } } } diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierServiceOptions.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierServiceOptions.ts index 763c64b750..701033a89b 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierServiceOptions.ts @@ -1,5 +1,6 @@ import type { OpenId4VcJwtIssuer, + OpenId4VcSiopAuthorizationRequestPayload, OpenId4VcSiopAuthorizationResponsePayload, OpenId4VcSiopIdTokenPayload, } from '../shared' @@ -33,6 +34,7 @@ export interface OpenId4VcSiopVerifyAuthorizationResponseOptions { export interface OpenId4VcSiopCreateAuthorizationRequestReturn { authorizationRequestUri: string + authorizationRequestPayload: OpenId4VcSiopAuthorizationRequestPayload } /** diff --git a/packages/openid4vc/src/shared/models/index.ts b/packages/openid4vc/src/shared/models/index.ts index 7f91d0a78c..dc37fafeb8 100644 --- a/packages/openid4vc/src/shared/models/index.ts +++ b/packages/openid4vc/src/shared/models/index.ts @@ -1,5 +1,6 @@ import type { VerifiedAuthorizationRequest, + AuthorizationRequestPayload, AuthorizationResponsePayload, IDTokenPayload, } from '@sphereon/did-auth-siop' @@ -27,6 +28,7 @@ export type OpenId4VciCredentialOffer = AssertedUniformCredentialOffer export type OpenId4VciCredentialOfferPayload = CredentialOfferPayloadV1_0_11 export type OpenId4VcSiopVerifiedAuthorizationRequest = VerifiedAuthorizationRequest +export type OpenId4VcSiopAuthorizationRequestPayload = AuthorizationRequestPayload export type OpenId4VcSiopAuthorizationResponsePayload = AuthorizationResponsePayload export type OpenId4VcSiopIdTokenPayload = IDTokenPayload diff --git a/packages/openid4vc/tests/openid4vc.e2e.test.ts b/packages/openid4vc/tests/openid4vc.e2e.test.ts index 93a78686c9..1233f89aa4 100644 --- a/packages/openid4vc/tests/openid4vc.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc.e2e.test.ts @@ -1,46 +1,38 @@ import type { AgentType, TenantType } from './utils' import type { OpenId4VciCredentialBindingResolver } from '../src/openid4vc-holder' -import type { SdJwtVc } from '@aries-framework/core' +import type { DifPresentationExchangeDefinitionV2, SdJwtVc } from '@aries-framework/core' import type { Server } from 'http' import { + AriesFrameworkError, ClaimFormat, + DidsApi, + DifPresentationExchangeService, + getJwkFromKey, + getKeyFromVerificationMethod, + JsonEncoder, JwaSignatureAlgorithm, W3cCredential, W3cCredentialSubject, - W3cIssuer, w3cDate, - DidsApi, - getKeyFromVerificationMethod, - getJwkFromKey, - AriesFrameworkError, - DifPresentationExchangeService, + W3cIssuer, } from '@aries-framework/core' import express, { type Express } from 'express' import { AskarModule } from '../../askar/src' import { askarModuleConfig } from '../../askar/tests/helpers' import { TenantsModule } from '../../tenants/src' -import { OpenId4VcVerifierModule, OpenId4VcHolderModule, OpenId4VcIssuerModule } from '../src' +import { OpenId4VcHolderModule, OpenId4VcIssuerModule, OpenId4VcVerifierModule } from '../src' import { createAgentFromModules, createTenantForAgent } from './utils' import { universityDegreeCredentialSdJwt, universityDegreeCredentialSdJwt2 } from './utilsVci' -import { - openBadgePresentationDefinition, - staticOpOpenIdConfigEdDSA, - universityDegreePresentationDefinition, -} from './utilsVp' +import { openBadgePresentationDefinition, universityDegreePresentationDefinition } from './utilsVp' const serverPort = 1234 const baseUrl = `http://localhost:${serverPort}` const issuanceBaseUrl = `${baseUrl}/oid4vci` const verificationBaseUrl = `${baseUrl}/oid4vp` -const baseCredentialOfferOptions = { - scheme: 'openid-credential-offer', - baseUri: issuanceBaseUrl, -} - describe('OpenId4Vc', () => { let expressApp: Express let expressServer: Server @@ -75,7 +67,6 @@ describe('OpenId4Vc', () => { baseUrl: issuanceBaseUrl, endpoints: { credential: { - // FIXME: should not be nested under the endpoint config, as it's also used for the non-endpoint part credentialRequestToCredentialMapper: async ({ agentContext, credentialRequest, holderBinding }) => { // We sign the request with the first did:key did we have const didsApi = agentContext.dependencyManager.resolve(DidsApi) @@ -187,18 +178,16 @@ describe('OpenId4Vc', () => { credentialsSupported: [universityDegreeCredentialSdJwt2], }) - const { credentialOfferUri: credentialOffer1 } = await issuerTenant1.modules.openId4VcIssuer.createCredentialOffer({ + const { credentialOffer: credentialOffer1 } = await issuerTenant1.modules.openId4VcIssuer.createCredentialOffer({ issuerId: openIdIssuerTenant1.issuerId, offeredCredentials: [universityDegreeCredentialSdJwt.id], preAuthorizedCodeFlowConfig: { userPinRequired: false }, - ...baseCredentialOfferOptions, }) - const { credentialOfferUri: credentialOffer2 } = await issuerTenant2.modules.openId4VcIssuer.createCredentialOffer({ + const { credentialOffer: credentialOffer2 } = await issuerTenant2.modules.openId4VcIssuer.createCredentialOffer({ issuerId: openIdIssuerTenant2.issuerId, offeredCredentials: [universityDegreeCredentialSdJwt2.id], preAuthorizedCodeFlowConfig: { userPinRequired: false }, - ...baseCredentialOfferOptions, }) await issuerTenant1.endSession() @@ -230,7 +219,7 @@ describe('OpenId4Vc', () => { expect(credentialsTenant1).toHaveLength(1) const compactSdJwtVcTenant1 = (credentialsTenant1[0] as SdJwtVc).compact - const sdJwtVcTenant1 = await holderTenant1.sdJwtVc.fromCompact(compactSdJwtVcTenant1) + const sdJwtVcTenant1 = holderTenant1.sdJwtVc.fromCompact(compactSdJwtVcTenant1) expect(sdJwtVcTenant1.payload.vct).toEqual('UniversityDegreeCredential') const resolvedCredentialOffer2 = await holderTenant1.modules.openId4VcHolder.resolveCredentialOffer( @@ -256,13 +245,13 @@ describe('OpenId4Vc', () => { expect(credentialsTenant2).toHaveLength(1) const compactSdJwtVcTenant2 = (credentialsTenant2[0] as SdJwtVc).compact - const sdJwtVcTenant2 = await holderTenant1.sdJwtVc.fromCompact(compactSdJwtVcTenant2) + const sdJwtVcTenant2 = holderTenant1.sdJwtVc.fromCompact(compactSdJwtVcTenant2) expect(sdJwtVcTenant2.payload.vct).toEqual('UniversityDegreeCredential2') await holderTenant1.endSession() }) - it('e2e flow with tenants, verifier endpoints verifying a sd-jwt-vc', async () => { + it('e2e flow with tenants, verifier endpoints verifying a jwt-vc', async () => { const holderTenant = await holder.agent.modules.tenants.getTenantAgent({ tenantId: holder1.tenantId }) const verifierTenant1 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) const verifierTenant2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier2.tenantId }) @@ -297,82 +286,108 @@ describe('OpenId4Vc', () => { await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential1 }) await holderTenant.w3cCredentials.storeCredential({ credential: signedCredential2 }) - const { authorizationRequestUri: authorizationRequestUri1, metadata: proofRequestMetadata1 } = - await verifierTenant1.modules.openId4VcVerifier.createAuthorizationRequest({ - verificationMethod: verifier1.verificationMethod, - openIdProvider: staticOpOpenIdConfigEdDSA, - presentationDefinition: openBadgePresentationDefinition, - verifierId: openIdVerifierTenant1.verifierId, - }) + const { + authorizationRequestUri: authorizationRequestUri1, + authorizationRequestPayload: authorizationRequestPayload1, + } = await verifierTenant1.modules.openId4VcVerifier.createAuthorizationRequest({ + verifierId: openIdVerifierTenant1.verifierId, + requestSigner: { + method: 'did', + didUrl: verifier1.verificationMethod.id, + }, + presentationDefinition: openBadgePresentationDefinition, + }) - // FIXME: the presentation definition is in both top-level and request param? expect( authorizationRequestUri1.startsWith( - `openid://?redirect_uri=http%3A%2F%2Flocalhost%3A1234%2Foid4vp%2F${openIdVerifierTenant1.verifierId}%2Fauthorize&presentation_definition=%7B%22id%22%3A%22OpenBadgeCredential` + `openid://?redirect_uri=http%3A%2F%2Flocalhost%3A1234%2Foid4vp%2F${openIdVerifierTenant1.verifierId}%2Fauthorize` ) ).toBe(true) - const { authorizationRequestUri: authorizationRequestUri2, metadata: proofRequestMetadata2 } = - await verifierTenant2.modules.openId4VcVerifier.createAuthorizationRequest({ - verificationMethod: verifier2.verificationMethod, - openIdProvider: staticOpOpenIdConfigEdDSA, - presentationDefinition: universityDegreePresentationDefinition, - verifierId: openIdVerifierTenant2.verifierId, - }) + const { + authorizationRequestUri: authorizationRequestUri2, + authorizationRequestPayload: authorizationRequestPayload2, + } = await verifierTenant2.modules.openId4VcVerifier.createAuthorizationRequest({ + requestSigner: { + method: 'did', + didUrl: verifier2.verificationMethod.id, + }, + presentationDefinition: universityDegreePresentationDefinition, + verifierId: openIdVerifierTenant2.verifierId, + }) - // FIXME: we should set scheme based on the openid provider metadata - // Is the op set in the request? - // FIXME: did:peer should not be supported expect( authorizationRequestUri2.startsWith( - `openid://?redirect_uri=http%3A%2F%2Flocalhost%3A1234%2Foid4vp%2F${openIdVerifierTenant2.verifierId}%2Fauthorize&presentation_definition=%7B%22id%22%3A%22UniversityDegreeCredential` + `openid://?redirect_uri=http%3A%2F%2Flocalhost%3A1234%2Foid4vp%2F${openIdVerifierTenant2.verifierId}%2Fauthorize` ) ).toBe(true) await verifierTenant1.endSession() await verifierTenant2.endSession() - // FIXME: api already has resolve authorization request - // but it's used for oid4vci. We should have some improvements on the api - const resolvedProofRequest1 = await holderTenant.modules.openId4VcHolder.resolveProofRequest( + const resolvedProofRequest1 = await holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest( authorizationRequestUri1 ) - if (resolvedProofRequest1.proofType === 'authentication') throw new Error('Expected a proofRequest') - - if (!resolvedProofRequest1.credentialsForRequest.areRequirementsSatisfied) { - throw new Error('Requirements are not satisfied.') - } - expect( - resolvedProofRequest1.credentialsForRequest.requirements[0].submissionEntry[0].verifiableCredentials[0].credential - .type - ).toContain('OpenBadgeCredential') + expect(resolvedProofRequest1.presentationExchange?.credentialsForRequest).toMatchObject({ + areRequirementsSatisfied: true, + requirements: [ + { + submissionEntry: [ + { + verifiableCredentials: [ + { + credential: { + type: ['VerifiableCredential', 'OpenBadgeCredential'], + }, + }, + ], + }, + ], + }, + ], + }) - const resolvedProofRequest2 = await holderTenant.modules.openId4VcHolder.resolveProofRequest( + const resolvedProofRequest2 = await holderTenant.modules.openId4VcHolder.resolveSiopAuthorizationRequest( authorizationRequestUri2 ) - if (resolvedProofRequest2.proofType === 'authentication') throw new Error('Expected a proofRequest') - if (!resolvedProofRequest2.credentialsForRequest.areRequirementsSatisfied) { - throw new Error('Requirements are not satisfied.') - } + expect(resolvedProofRequest2.presentationExchange?.credentialsForRequest).toMatchObject({ + areRequirementsSatisfied: true, + requirements: [ + { + submissionEntry: [ + { + verifiableCredentials: [ + { + credential: { + type: ['VerifiableCredential', 'UniversityDegreeCredential'], + }, + }, + ], + }, + ], + }, + ], + }) - // FIXME: result MUST include SD-JWT as well - expect( - resolvedProofRequest2.credentialsForRequest.requirements[0].submissionEntry[0].verifiableCredentials[0].credential - .type - ).toContain('UniversityDegreeCredential') + if (!resolvedProofRequest1.presentationExchange || !resolvedProofRequest2.presentationExchange) { + throw new Error('Presentation exchange not defined') + } const presentationExchangeService = holderTenant.dependencyManager.resolve(DifPresentationExchangeService) const selectedCredentials = presentationExchangeService.selectCredentialsForRequest( - resolvedProofRequest1.credentialsForRequest + resolvedProofRequest1.presentationExchange.credentialsForRequest ) const { status: status1, submittedResponse: submittedResponse1 } = - await holderTenant.modules.openId4VcHolder.acceptPresentationRequest( - resolvedProofRequest1.presentationRequest, - selectedCredentials - ) + await holderTenant.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedProofRequest1.authorizationRequest, + presentationExchange: { + credentials: selectedCredentials, + }, + }) + expect(submittedResponse1).toEqual({ expires_in: 6000, id_token: expect.any(String), @@ -396,20 +411,20 @@ describe('OpenId4Vc', () => { // that the RP sent in the Authorization Request as an audience. // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. const verifierTenant1_2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier1.tenantId }) - const { idTokenPayload: idTokenPayload1, presentationExchange: presentationExchange1 } = + const { idToken: idToken1, presentationExchange: presentationExchange1 } = await verifierTenant1_2.modules.openId4VcVerifier.verifyAuthorizationResponse({ authorizationResponse: submittedResponse1, verifierId: openIdVerifierTenant1.verifierId, }) - const { state: state1, nonce: nonce1 } = proofRequestMetadata1 - expect(idTokenPayload1).toMatchObject({ - state: state1, - nonce: nonce1, + const requestObjectPayload1 = JsonEncoder.fromBase64(authorizationRequestPayload1.request?.split('.')[1] as string) + expect(idToken1?.payload).toMatchObject({ + state: requestObjectPayload1.state, + nonce: requestObjectPayload1.nonce, }) expect(presentationExchange1).toMatchObject({ - definitions: [openBadgePresentationDefinition], + definition: openBadgePresentationDefinition, submission: { definition_id: 'OpenBadgeCredential', }, @@ -425,35 +440,36 @@ describe('OpenId4Vc', () => { }) const selectedCredentials2 = presentationExchangeService.selectCredentialsForRequest( - resolvedProofRequest2.credentialsForRequest + resolvedProofRequest2.presentationExchange.credentialsForRequest ) - // FIXME: do we want to return the submitted response? And the status code? - // Also, do we get anything back for submitting this? const { status: status2, submittedResponse: submittedResponse2 } = - await holderTenant.modules.openId4VcHolder.acceptPresentationRequest( - resolvedProofRequest2.presentationRequest, - selectedCredentials2 - ) + await holderTenant.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedProofRequest2.authorizationRequest, + presentationExchange: { + credentials: selectedCredentials2, + }, + }) expect(status2).toBe(200) // The RP MUST validate that the aud (audience) Claim contains the value of the client_id // that the RP sent in the Authorization Request as an audience. // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. const verifierTenant2_2 = await verifier.agent.modules.tenants.getTenantAgent({ tenantId: verifier2.tenantId }) - const { idTokenPayload: idTokenPayload2, presentationExchange: presentationExchange2 } = + const { idToken: idToken2, presentationExchange: presentationExchange2 } = await verifierTenant2_2.modules.openId4VcVerifier.verifyAuthorizationResponse({ authorizationResponse: submittedResponse2, verifierId: openIdVerifierTenant2.verifierId, }) - expect(idTokenPayload2).toMatchObject({ - state: proofRequestMetadata2.state, - nonce: proofRequestMetadata2.nonce, + const requestObjectPayload2 = JsonEncoder.fromBase64(authorizationRequestPayload2.request?.split('.')[1] as string) + expect(idToken2?.payload).toMatchObject({ + state: requestObjectPayload2.state, + nonce: requestObjectPayload2.nonce, }) expect(presentationExchange2).toMatchObject({ - definitions: [universityDegreePresentationDefinition], + definition: universityDegreePresentationDefinition, submission: { definition_id: 'UniversityDegreeCredential', }, @@ -468,4 +484,193 @@ describe('OpenId4Vc', () => { ], }) }) + + // FIXME: test whether did-based binding for holder works with sd-jwt-vc library in verification + // issuance is fine, but in verification of KB the jwk will be extracted + // from the cnf claim which will be undefined. + it('e2e flow with verifier endpoints verifying a sd-jwt-vc with selective disclosure', async () => { + const openIdVerifier = await verifier.agent.modules.openId4VcVerifier.createVerifier() + + const signedSdJwtVc = await issuer.agent.sdJwtVc.sign({ + holder: { + method: 'did', + didUrl: holder.kid, + }, + issuer: { + method: 'did', + didUrl: issuer.kid, + }, + payload: { + vct: 'OpenBadgeCredential', + university: 'innsbruck', + degree: 'bachelor', + name: 'John Doe', + }, + disclosureFrame: { + university: true, + name: true, + }, + }) + + await holder.agent.sdJwtVc.store(signedSdJwtVc.compact) + + const presentationDefinition = { + id: 'OpenBadgeCredential', + input_descriptors: [ + { + id: 'OpenBadgeCredential', + // FIXME: https://github.com/Sphereon-Opensource/pex-openapi/issues/32 + // format: { + // 'vc+sd-jwt': { + // 'sd-jwt_alg_values': ['EdDSA'], + // }, + // }, + constraints: { + limit_disclosure: 'required', + fields: [ + { + path: ['$.vct'], + filter: { + type: 'string', + const: 'OpenBadgeCredential', + }, + }, + { + path: ['$.university'], + }, + ], + }, + }, + ], + } satisfies DifPresentationExchangeDefinitionV2 + + const { authorizationRequestUri, authorizationRequestPayload } = + await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ + verifierId: openIdVerifier.verifierId, + requestSigner: { + method: 'did', + didUrl: verifier.kid, + }, + presentationDefinition, + }) + + expect( + authorizationRequestUri.startsWith( + `openid://?redirect_uri=http%3A%2F%2Flocalhost%3A1234%2Foid4vp%2F${openIdVerifier.verifierId}%2Fauthorize` + ) + ).toBe(true) + + const resolvedAuthorizationRequest = await holder.agent.modules.openId4VcHolder.resolveSiopAuthorizationRequest( + authorizationRequestUri + ) + + expect(resolvedAuthorizationRequest.presentationExchange?.credentialsForRequest).toMatchObject({ + areRequirementsSatisfied: true, + requirements: [ + { + submissionEntry: [ + { + verifiableCredentials: [ + { + // FIXME: because we have the record, we don't know which fields will be disclosed + // Can we temp-assign these to the record? + compactSdJwtVc: signedSdJwtVc.compact, + }, + ], + }, + ], + }, + ], + }) + + if (!resolvedAuthorizationRequest.presentationExchange) { + throw new Error('Presentation exchange not defined') + } + + // TODO: better way to auto-select + const presentationExchangeService = holder.agent.dependencyManager.resolve(DifPresentationExchangeService) + const selectedCredentials = presentationExchangeService.selectCredentialsForRequest( + resolvedAuthorizationRequest.presentationExchange.credentialsForRequest + ) + + const { status, submittedResponse } = await holder.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedAuthorizationRequest.authorizationRequest, + presentationExchange: { + credentials: selectedCredentials, + }, + }) + + expect(submittedResponse).toEqual({ + expires_in: 6000, + id_token: expect.any(String), + presentation_submission: { + definition_id: 'OpenBadgeCredential', + descriptor_map: [ + { + format: 'vc+sd-jwt', + id: 'OpenBadgeCredential', + path: '$', + // FIXME: sd-jwt should not use path_nested + path_nested: { + format: 'vc+sd-jwt', + id: 'OpenBadgeCredential', + path: '$', + }, + }, + ], + id: expect.any(String), + }, + state: expect.any(String), + vp_token: expect.any(String), + }) + expect(status).toBe(200) + + // FIXME: we need https://github.com/Sphereon-Opensource/SIOP-OID4VP/pull/70 + // to be released as verification currently doesn't work + + // The RP MUST validate that the aud (audience) Claim contains the value of the client_id + // that the RP sent in the Authorization Request as an audience. + // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. + const { idToken, presentationExchange } = + await verifier.agent.modules.openId4VcVerifier.verifyAuthorizationResponse({ + authorizationResponse: submittedResponse, + verifierId: openIdVerifier.verifierId, + }) + + const requestObjectPayload = JsonEncoder.fromBase64(authorizationRequestPayload.request?.split('.')[1] as string) + expect(idToken?.payload).toMatchObject({ + state: requestObjectPayload.state, + nonce: requestObjectPayload.nonce, + }) + + const presentation = presentationExchange?.presentations[0] as SdJwtVc + + // name SHOULD NOT be disclosed + expect(presentation.prettyClaims).not.toHaveProperty('name') + + // university and name SHOULD NOT be in the signed payload + expect(presentation.payload).not.toHaveProperty('university') + expect(presentation.payload).not.toHaveProperty('name') + + // FIXME: we use definition here, but presentationDefinition elsewhere + expect(presentationExchange).toMatchObject({ + definition: presentationDefinition, + submission: { + definition_id: 'OpenBadgeCredential', + }, + presentations: [ + { + payload: { + vct: 'OpenBadgeCredential', + degree: 'bachelor', + }, + // university SHOULD be disclosed + prettyClaims: { + degree: 'bachelor', + university: 'innsbruck', + }, + }, + ], + }) + }) }) diff --git a/yarn.lock b/yarn.lock index 0460bc2a93..9262f00d5d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3109,7 +3109,7 @@ dependencies: "@types/express" "*" -"@types/node@*", "@types/node@18.18.8", "@types/node@>=13.7.0", "@types/node@^18.18.8": +"@types/node@*", "@types/node@>=13.7.0", "@types/node@^18.18.8": version "18.18.8" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.8.tgz#2b285361f2357c8c8578ec86b5d097c7f464cfd6" integrity sha512-OLGBaaK5V3VRBS1bAkMVP2/W9B+H8meUfl866OrMNQqt7wDgdpWPp5o6gmIc9pB+lIQHSq4ZL8ypeH1vPxcPaQ== From 6754b3035f3b4dde77e33f0ca63a6024d8592383 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sun, 28 Jan 2024 16:26:32 +0700 Subject: [PATCH 113/115] fixmes are fixed Signed-off-by: Timo Glastra --- .../DifPresentationExchangeService.ts | 33 ++++++++++++--- .../OpenId4vcSiopHolderService.ts | 1 - .../OpenId4VcSiopVerifierService.ts | 2 +- .../OpenId4VcSiopVerifierServiceOptions.ts | 4 +- .../openid4vc/tests/openid4vc.e2e.test.ts | 42 +++++++++---------- packages/openid4vc/tests/utilsVp.ts | 2 +- 6 files changed, 54 insertions(+), 30 deletions(-) diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts index 608304c22d..673cb7c31d 100644 --- a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts @@ -145,7 +145,9 @@ export class DifPresentationExchangeService { domain?: string } ) { - const { presentationDefinition, domain, challenge, presentationSubmissionLocation } = options + const { presentationDefinition, domain, challenge } = options + const presentationSubmissionLocation = + options.presentationSubmissionLocation ?? DifPresentationExchangeSubmissionLocation.PRESENTATION const verifiablePresentationResultsWithFormat: Array<{ verifiablePresentationResult: VerifiablePresentationResult @@ -208,10 +210,31 @@ export class DifPresentationExchangeService { descriptor_map: [], } - for (const vpf of verifiablePresentationResultsWithFormat) { - const { verifiablePresentationResult } = vpf - presentationSubmission.descriptor_map.push(...verifiablePresentationResult.presentationSubmission.descriptor_map) - } + verifiablePresentationResultsWithFormat.forEach(({ verifiablePresentationResult }, index) => { + // FIXME: path_nested should not be used for sd-jwt. + // Can be removed once https://github.com/Sphereon-Opensource/PEX/pull/140 is released + const descriptorMap = verifiablePresentationResult.presentationSubmission.descriptor_map.map((d) => { + const descriptor = { ...d } + + // when multiple presentations are submitted, path should be $[0], $[1] + // FIXME: this should be addressed in the PEX/OID4VP lib. + // See https://github.com/Sphereon-Opensource/SIOP-OID4VP/issues/62 + if ( + presentationSubmissionLocation === DifPresentationExchangeSubmissionLocation.EXTERNAL && + verifiablePresentationResultsWithFormat.length > 1 + ) { + descriptor.path = `$[${index}]` + } + + if (descriptor.format === 'vc+sd-jwt' && descriptor.path_nested) { + delete descriptor.path_nested + } + + return descriptor + }) + + presentationSubmission.descriptor_map.push(...descriptorMap) + }) return { verifiablePresentations: verifiablePresentationResultsWithFormat.map((resultWithFormat) => diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts index eec4364477..23d5104e57 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts @@ -97,7 +97,6 @@ export class OpenId4VcSiopHolderService { ) } - // FIXME: make sure nonce and clientId are also verified in the verify proof method const nonce = await authorizationRequest.authorizationRequest.getMergedProperty('nonce') if (!nonce) { throw new AriesFrameworkError("Unable to extract 'nonce' from authorization request") diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts index 07bafa3c88..4d04802de9 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts @@ -70,7 +70,7 @@ export class OpenId4VcSiopVerifierService { const correlationId = utils.uuid() const relyingParty = await this.getRelyingParty(agentContext, options.verifier, { - presentationDefinition: options.presentationDefinition, + presentationDefinition: options.presentationExchange?.definition, requestSigner: options.requestSigner, }) diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierServiceOptions.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierServiceOptions.ts index 701033a89b..e99a8179e8 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierServiceOptions.ts @@ -22,7 +22,9 @@ export interface OpenId4VcSiopCreateAuthorizationRequestOptions { /** * A DIF Presentation Definition (v2) can be provided to request a Verifiable Presentation using OpenID4VP. */ - presentationDefinition?: DifPresentationExchangeDefinitionV2 + presentationExchange?: { + definition: DifPresentationExchangeDefinitionV2 + } } export interface OpenId4VcSiopVerifyAuthorizationResponseOptions { diff --git a/packages/openid4vc/tests/openid4vc.e2e.test.ts b/packages/openid4vc/tests/openid4vc.e2e.test.ts index 1233f89aa4..7039161bb5 100644 --- a/packages/openid4vc/tests/openid4vc.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc.e2e.test.ts @@ -295,7 +295,9 @@ describe('OpenId4Vc', () => { method: 'did', didUrl: verifier1.verificationMethod.id, }, - presentationDefinition: openBadgePresentationDefinition, + presentationExchange: { + definition: openBadgePresentationDefinition, + }, }) expect( @@ -312,7 +314,9 @@ describe('OpenId4Vc', () => { method: 'did', didUrl: verifier2.verificationMethod.id, }, - presentationDefinition: universityDegreePresentationDefinition, + presentationExchange: { + definition: universityDegreePresentationDefinition, + }, verifierId: openIdVerifierTenant2.verifierId, }) @@ -395,9 +399,14 @@ describe('OpenId4Vc', () => { definition_id: 'OpenBadgeCredential', descriptor_map: [ { - format: 'jwt_vc', - id: 'OpenBadgeCredential', - path: '$.verifiableCredential[0]', + format: 'jwt_vp', + id: 'OpenBadgeCredentialDescriptor', + path: '$', + path_nested: { + format: 'jwt_vc', + id: 'OpenBadgeCredentialDescriptor', + path: '$.vp.verifiableCredential[0]', + }, }, ], id: expect.any(String), @@ -485,9 +494,6 @@ describe('OpenId4Vc', () => { }) }) - // FIXME: test whether did-based binding for holder works with sd-jwt-vc library in verification - // issuance is fine, but in verification of KB the jwk will be extracted - // from the cnf claim which will be undefined. it('e2e flow with verifier endpoints verifying a sd-jwt-vc with selective disclosure', async () => { const openIdVerifier = await verifier.agent.modules.openId4VcVerifier.createVerifier() @@ -518,7 +524,7 @@ describe('OpenId4Vc', () => { id: 'OpenBadgeCredential', input_descriptors: [ { - id: 'OpenBadgeCredential', + id: 'OpenBadgeCredentialDescriptor', // FIXME: https://github.com/Sphereon-Opensource/pex-openapi/issues/32 // format: { // 'vc+sd-jwt': { @@ -551,7 +557,9 @@ describe('OpenId4Vc', () => { method: 'did', didUrl: verifier.kid, }, - presentationDefinition, + presentationExchange: { + definition: presentationDefinition, + }, }) expect( @@ -600,6 +608,8 @@ describe('OpenId4Vc', () => { }, }) + // path_nested should not be used for sd-jwt + expect(submittedResponse.presentation_submission?.descriptor_map[0].path_nested).toBeUndefined() expect(submittedResponse).toEqual({ expires_in: 6000, id_token: expect.any(String), @@ -608,14 +618,8 @@ describe('OpenId4Vc', () => { descriptor_map: [ { format: 'vc+sd-jwt', - id: 'OpenBadgeCredential', + id: 'OpenBadgeCredentialDescriptor', path: '$', - // FIXME: sd-jwt should not use path_nested - path_nested: { - format: 'vc+sd-jwt', - id: 'OpenBadgeCredential', - path: '$', - }, }, ], id: expect.any(String), @@ -625,9 +629,6 @@ describe('OpenId4Vc', () => { }) expect(status).toBe(200) - // FIXME: we need https://github.com/Sphereon-Opensource/SIOP-OID4VP/pull/70 - // to be released as verification currently doesn't work - // The RP MUST validate that the aud (audience) Claim contains the value of the client_id // that the RP sent in the Authorization Request as an audience. // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. @@ -652,7 +653,6 @@ describe('OpenId4Vc', () => { expect(presentation.payload).not.toHaveProperty('university') expect(presentation.payload).not.toHaveProperty('name') - // FIXME: we use definition here, but presentationDefinition elsewhere expect(presentationExchange).toMatchObject({ definition: presentationDefinition, submission: { diff --git a/packages/openid4vc/tests/utilsVp.ts b/packages/openid4vc/tests/utilsVp.ts index eb9713f0f3..229eba683e 100644 --- a/packages/openid4vc/tests/utilsVp.ts +++ b/packages/openid4vc/tests/utilsVp.ts @@ -83,7 +83,7 @@ export const openBadgePresentationDefinition: DifPresentationExchangeDefinitionV id: 'OpenBadgeCredential', input_descriptors: [ { - id: 'OpenBadgeCredential', + id: 'OpenBadgeCredentialDescriptor', // changed jwt_vc_json to jwt_vc format: { jwt_vc: { alg: ['EdDSA'] } }, // changed $.type to $.vc.type From 226de983840ffbb94001b960f9a534866bd8fab3 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sun, 28 Jan 2024 17:53:01 +0700 Subject: [PATCH 114/115] it actually compiles and runs!! Signed-off-by: Timo Glastra --- demo-openid/src/Holder.ts | 44 +- demo-openid/src/HolderInquirer.ts | 96 +-- demo-openid/src/Issuer.ts | 27 +- demo-openid/src/Verifier.ts | 115 ++-- demo-openid/src/VerifierInquirer.ts | 1 - .../models/W3cJsonLdVerifiableCredential.ts | 5 + .../vc/jwt-vc/W3cJwtVerifiableCredential.ts | 6 + .../OpenId4vcSiopHolderService.ts | 20 +- ....test.ts => OpenId4VcHolderModule.test.ts} | 0 .../__tests__/openid4vci-holder.e2e.test.ts | 6 +- .../__tests__/openid4vp-holder.e2e.test.ts | 620 ++---------------- ...e.test.ts => OpenId4VcIsserModule.test.ts} | 0 .../OpenId4VcSiopVerifierService.ts | 6 +- ...est.ts => OpenId4VcVerifierModule.test.ts} | 31 +- .../__tests__/openid4vc-verifier.e2e.test.ts | 85 +-- .../openid4vc/src/openid4vc-verifier/index.ts | 1 + .../openid4vc/tests/openid4vc.e2e.test.ts | 29 +- tsconfig.test.json | 3 + yarn.lock | 2 +- 19 files changed, 295 insertions(+), 802 deletions(-) rename packages/openid4vc/src/openid4vc-holder/__tests__/{openId4vc-holder-module.test.ts => OpenId4VcHolderModule.test.ts} (100%) rename packages/openid4vc/src/openid4vc-issuer/__tests__/{openId4vc-issuer-module.test.ts => OpenId4VcIsserModule.test.ts} (100%) rename packages/openid4vc/src/openid4vc-verifier/__tests__/{openId4vc-verifier-module.test.ts => OpenId4VcVerifierModule.test.ts} (56%) diff --git a/demo-openid/src/Holder.ts b/demo-openid/src/Holder.ts index a4867c14ca..8a661b4825 100644 --- a/demo-openid/src/Holder.ts +++ b/demo-openid/src/Holder.ts @@ -1,11 +1,14 @@ import type { - OfferedCredentialWithMetadata, - ResolvedPresentationRequest, - ResolvedCredentialOffer, + OpenId4VciResolvedCredentialOffer, + OpenId4VcSiopResolvedAuthorizationRequest, } from '@aries-framework/openid4vc' import { AskarModule } from '@aries-framework/askar' -import { W3cJwtVerifiableCredential, W3cJsonLdVerifiableCredential } from '@aries-framework/core' +import { + W3cJwtVerifiableCredential, + W3cJsonLdVerifiableCredential, + DifPresentationExchangeService, +} from '@aries-framework/core' import { OpenId4VcHolderModule } from '@aries-framework/openid4vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' @@ -36,8 +39,8 @@ export class Holder extends BaseAgent> } public async requestAndStoreCredentials( - resolvedCredentialOffer: ResolvedCredentialOffer, - credentialsToRequest: OfferedCredentialWithMetadata[] + resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer, + credentialsToRequest: string[] ) { const credentials = await this.agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( resolvedCredentialOffer, @@ -65,25 +68,28 @@ export class Holder extends BaseAgent> } public async resolveProofRequest(proofRequest: string) { - const resolvedProofRequest = await this.agent.modules.openId4VcHolder.resolveProofRequest(proofRequest) - - if (resolvedProofRequest.proofType === 'authentication') - throw new Error('We only support presentation requests for now.') + const resolvedProofRequest = await this.agent.modules.openId4VcHolder.resolveSiopAuthorizationRequest(proofRequest) return resolvedProofRequest } - public async acceptPresentationRequest( - resolvedPresentationRequest: ResolvedPresentationRequest, - submissionEntryIndexes: number[] - ) { - const { presentationRequest, presentationSubmission } = resolvedPresentationRequest - const submissionResult = await this.agent.modules.openId4VcHolder.acceptPresentationRequest(presentationRequest, { - submission: presentationSubmission, - submissionEntryIndexes, + public async acceptPresentationRequest(resolvedPresentationRequest: OpenId4VcSiopResolvedAuthorizationRequest) { + const presentationExchangeService = this.agent.dependencyManager.resolve(DifPresentationExchangeService) + + if (!resolvedPresentationRequest.presentationExchange) { + throw new Error('Missing presentation exchange on resolved authorization request') + } + + const submissionResult = await this.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedPresentationRequest.authorizationRequest, + presentationExchange: { + credentials: presentationExchangeService.selectCredentialsForRequest( + resolvedPresentationRequest.presentationExchange.credentialsForRequest + ), + }, }) - return submissionResult.status + return submissionResult.serverResponse } public async exit() { diff --git a/demo-openid/src/HolderInquirer.ts b/demo-openid/src/HolderInquirer.ts index a28df6a6d0..a4fe1cf1e6 100644 --- a/demo-openid/src/HolderInquirer.ts +++ b/demo-openid/src/HolderInquirer.ts @@ -1,6 +1,11 @@ -import type { ResolvedCredentialOffer, ResolvedPresentationRequest } from '@aries-framework/openid4vc' - -import { clear } from 'console' +import type { SdJwtVcRecord, W3cCredentialRecord } from '@aries-framework/core/src' +import type { + OpenId4VcSiopResolvedAuthorizationRequest, + OpenId4VciResolvedCredentialOffer, +} from '@aries-framework/openid4vc' + +import { DifPresentationExchangeService } from '@aries-framework/core/src' +import console, { clear } from 'console' import { textSync } from 'figlet' import { prompt } from 'inquirer' @@ -26,8 +31,8 @@ enum PromptOptions { export class HolderInquirer extends BaseInquirer { public holder: Holder - public resolvedCredentialOffer?: ResolvedCredentialOffer - public resolvedPresentationRequest?: ResolvedPresentationRequest + public resolvedCredentialOffer?: OpenId4VciResolvedCredentialOffer + public resolvedPresentationRequest?: OpenId4VcSiopResolvedAuthorizationRequest public constructor(holder: Holder) { super() @@ -89,9 +94,7 @@ export class HolderInquirer extends BaseInquirer { this.resolvedCredentialOffer = resolvedCredentialOffer console.log(greenText(`Received credential offer for the following credentials.`)) - console.log( - greenText(resolvedCredentialOffer.offeredCredentials.map((credential) => credential.types.join(', ')).join('\n')) - ) + console.log(greenText(resolvedCredentialOffer.offeredCredentials.map((credential) => credential.id).join('\n'))) } public async requestCredential() { @@ -99,49 +102,49 @@ export class HolderInquirer extends BaseInquirer { throw new Error('No credential offer resolved yet.') } - const credentialsThatCanBeRequested = this.resolvedCredentialOffer.offeredCredentials.map((credential) => - credential.types.join(', ') + const credentialsThatCanBeRequested = this.resolvedCredentialOffer.offeredCredentials.map( + (credential) => credential.id ) const choice = await prompt([this.inquireOptions(credentialsThatCanBeRequested)]) const credentialToRequest = this.resolvedCredentialOffer.offeredCredentials.find( - (credential) => credential.types.join(', ') == choice.options + (credential) => credential.id === choice.options ) if (!credentialToRequest) throw new Error('Credential to request not found.') - console.log(greenText(`Requesting the following credential '${credentialToRequest.types.join(', ')}'`)) + console.log(greenText(`Requesting the following credential '${credentialToRequest.id}'`)) const credentials = await this.holder.requestAndStoreCredentials( this.resolvedCredentialOffer, - this.resolvedCredentialOffer.offeredCredentials - ) - - const credentialTypes = await Promise.all( - credentials.map((credential) => - credential.type === 'W3cCredentialRecord' - ? `${credential.credential.type.join(', ')}, CredentialType: W3cVerifiableCredential` - : this.holder.agent.sdJwtVc - .fromCompact(credential.compactSdJwtVc) - .then((a) => `${a.prettyClaims.vct}, CredentialType: SdJwtVc`) - ) + this.resolvedCredentialOffer.offeredCredentials.map((o) => o.id) ) console.log(greenText(`Received and stored the following credentials.`)) - console.log(greenText(credentialTypes.join('\n'))) + console.log('') + credentials.forEach(this.printCredential) } public async resolveProofRequest() { const proofRequestUri = await prompt([this.inquireInput('Enter proof request: ')]) this.resolvedPresentationRequest = await this.holder.resolveProofRequest(proofRequestUri.input) - const presentationDefinition = - this.resolvedPresentationRequest.presentationRequest.presentationDefinitions[0].definition - - console.log(greenText(`Presentation Purpose: '${presentationDefinition.purpose}'`)) - - if (this.resolvedPresentationRequest.presentationSubmission.areRequirementsSatisfied) { - console.log(greenText(`All requirements for creating the presentation are satisfied.`)) + const presentationDefinition = this.resolvedPresentationRequest?.presentationExchange?.definition + console.log(greenText(`Presentation Purpose: '${presentationDefinition?.purpose}'`)) + + if (this.resolvedPresentationRequest?.presentationExchange?.credentialsForRequest.areRequirementsSatisfied) { + const selectedCredentials = Object.values( + this.holder.agent.dependencyManager + .resolve(DifPresentationExchangeService) + .selectCredentialsForRequest(this.resolvedPresentationRequest.presentationExchange.credentialsForRequest) + ).flatMap((e) => e) + console.log( + greenText( + `All requirements for creating the presentation are satisfied. The following credentials will be shared`, + true + ) + ) + selectedCredentials.forEach(this.printCredential) } else { console.log(redText(`No credentials available that satisfy the proof request.`)) } @@ -150,22 +153,14 @@ export class HolderInquirer extends BaseInquirer { public async acceptPresentationRequest() { if (!this.resolvedPresentationRequest) throw new Error('No presentation request resolved yet.') - // we know that only one credential is in the wallet and it satisfies the proof request. - // The submission entry index for this credential is 0. - const credential = - this.resolvedPresentationRequest.presentationSubmission.requirements[0].submissionEntry[0] - .verifiableCredentials[0] - const submissionEntryIndexes = [0] - - console.log(greenText(`Accepting the presentation request, with the following credential.`)) - console.log(greenText(credential.credential.type.join(', '))) + console.log(greenText(`Accepting the presentation request.`)) - const status = await this.holder.acceptPresentationRequest(this.resolvedPresentationRequest, submissionEntryIndexes) + const serverResponse = await this.holder.acceptPresentationRequest(this.resolvedPresentationRequest) - if (status >= 200 && status < 300) { - console.log(`received success status code '${status}'`) + if (serverResponse.status >= 200 && serverResponse.status < 300) { + console.log(`received success status code '${serverResponse.status}'`) } else { - console.log(`received error status code '${status}'`) + console.log(`received error status code '${serverResponse.status}'`) } } @@ -188,6 +183,19 @@ export class HolderInquirer extends BaseInquirer { await runHolder() } } + + private printCredential = (credential: W3cCredentialRecord | SdJwtVcRecord) => { + if (credential.type === 'W3cCredentialRecord') { + console.log(greenText(`W3cCredentialRecord with claim format ${credential.credential.claimFormat}`, true)) + console.log(JSON.stringify(credential.credential.jsonCredential, null, 2)) + console.log('') + } else { + console.log(greenText(`SdJwtVcRecord`, true)) + const prettyClaims = this.holder.agent.sdJwtVc.fromCompact(credential.compactSdJwtVc).prettyClaims + console.log(JSON.stringify(prettyClaims, null, 2)) + console.log('') + } + } } void runHolder() diff --git a/demo-openid/src/Issuer.ts b/demo-openid/src/Issuer.ts index 56daaeb874..0eb75be86f 100644 --- a/demo-openid/src/Issuer.ts +++ b/demo-openid/src/Issuer.ts @@ -1,14 +1,15 @@ import type { DidKey } from '@aries-framework/core' import type { - CredentialHolderBinding, - CredentialHolderDidBinding, - CredentialRequestToCredentialMapper, + OpenId4VcCredentialHolderBinding, + OpenId4VcCredentialHolderDidBinding, + OpenId4VciCredentialRequestToCredentialMapper, OpenId4VciCredentialSupportedWithId, OpenId4VcIssuerRecord, } from '@aries-framework/openid4vc' import { AskarModule } from '@aries-framework/askar' import { + ClaimFormat, parseDid, AriesFrameworkError, W3cCredential, @@ -51,7 +52,7 @@ function getCredentialRequestToCredentialMapper({ issuerDidKey, }: { issuerDidKey: DidKey -}): CredentialRequestToCredentialMapper { +}): OpenId4VciCredentialRequestToCredentialMapper { return async ({ holderBinding, credentialsSupported }) => { const credentialSupported = credentialsSupported[0] @@ -59,6 +60,7 @@ function getCredentialRequestToCredentialMapper({ assertDidBasedHolderBinding(holderBinding) return { + format: ClaimFormat.JwtVc, credential: new W3cCredential({ type: universityDegreeCredential.types, issuer: new W3cIssuer({ @@ -77,6 +79,7 @@ function getCredentialRequestToCredentialMapper({ assertDidBasedHolderBinding(holderBinding) return { + format: ClaimFormat.JwtVc, credential: new W3cCredential({ type: openBadgeCredential.types, issuer: new W3cIssuer({ @@ -93,6 +96,7 @@ function getCredentialRequestToCredentialMapper({ if (credentialSupported.id === universityDegreeCredentialSdJwt.id) { return { + format: ClaimFormat.SdJwtVc, payload: { vct: universityDegreeCredentialSdJwt.vct, university: 'innsbruck', degree: 'bachelor' }, holder: holderBinding, issuer: { @@ -131,7 +135,7 @@ export class Issuer extends BaseAgent<{ }, }, }), - } as const, + }, }) this.app.use('/oid4vci', openId4VciRouter) @@ -148,14 +152,13 @@ export class Issuer extends BaseAgent<{ } public async createCredentialOffer(offeredCredentials: string[]) { - const { credentialOfferUri } = await this.agent.modules.openId4VcIssuer.createCredentialOffer({ + const { credentialOffer } = await this.agent.modules.openId4VcIssuer.createCredentialOffer({ issuerId: this.issuerRecord.issuerId, offeredCredentials, - scheme: 'openid-credential-offer', preAuthorizedCodeFlowConfig: { userPinRequired: false }, }) - return credentialOfferUri + return credentialOffer } public async exit() { @@ -170,7 +173,9 @@ export class Issuer extends BaseAgent<{ } function assertDidBasedHolderBinding( - holderBinding: CredentialHolderBinding -): asserts holderBinding is CredentialHolderDidBinding { - throw new AriesFrameworkError('Only did based holder bindings supported for this credential type') + holderBinding: OpenId4VcCredentialHolderBinding +): asserts holderBinding is OpenId4VcCredentialHolderDidBinding { + if (holderBinding.method !== 'did') { + throw new AriesFrameworkError('Only did based holder bindings supported for this credential type') + } } diff --git a/demo-openid/src/Verifier.ts b/demo-openid/src/Verifier.ts index 85b758ddcf..8ab4002d74 100644 --- a/demo-openid/src/Verifier.ts +++ b/demo-openid/src/Verifier.ts @@ -1,13 +1,8 @@ -import type { - ProofResponseHandler, - CreateProofRequestOptions, - VerifierEndpointConfig, - PresentationDefinitionV2, -} from '@aries-framework/openid4vc' -import type e from 'express' +import type { DifPresentationExchangeDefinitionV2 } from '@aries-framework/core/src' +import type { OpenId4VcVerifierRecord } from '@aries-framework/openid4vc' import { AskarModule } from '@aries-framework/askar' -import { SigningAlgo, OpenId4VcVerifierModule, staticOpOpenIdConfig } from '@aries-framework/openid4vc' +import { OpenId4VcVerifierModule } from '@aries-framework/openid4vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { Router } from 'express' @@ -19,12 +14,18 @@ const universityDegreePresentationDefinition = { purpose: 'Present your UniversityDegreeCredential to verify your education level.', input_descriptors: [ { - id: 'UniversityDegreeCredential', - // changed jwt_vc_json to jwt_vc - format: { jwt_vc: { alg: ['EdDSA'] } }, - // changed $.type to $.vc.type + id: 'UniversityDegreeCredentialDescriptor', constraints: { - fields: [{ path: ['$.vc.type.*'], filter: { type: 'string', pattern: 'UniversityDegree' } }], + fields: [ + { + // Works for JSON-LD, SD-JWT and JWT + path: ['$.vc.type.*', '$.vct', '$.type'], + filter: { + type: 'string', + pattern: 'UniversityDegree', + }, + }, + ], }, }, ], @@ -35,12 +36,18 @@ const openBadgeCredentialPresentationDefinition = { purpose: 'Provide proof of employment to confirm your employment status.', input_descriptors: [ { - id: 'OpenBadgeCredential', - // changed jwt_vc_json to jwt_vc - format: { jwt_vc: { alg: ['EdDSA'] } }, - // changed $.type to $.vc.type + id: 'OpenBadgeCredentialDescriptor', constraints: { - fields: [{ path: ['$.vc.type.*'], filter: { type: 'string', pattern: 'OpenBadgeCredential' } }], + fields: [ + { + // Works for JSON-LD, SD-JWT and JWT + path: ['$.vc.type.*', '$.vct', '$.type'], + filter: { + type: 'string', + pattern: 'OpenBadgeCredential', + }, + }, + ], }, }, ], @@ -51,64 +58,48 @@ export const presentationDefinitions = [ openBadgeCredentialPresentationDefinition, ] -function getOpenIdVerifierModules() { - return { - askar: new AskarModule({ ariesAskar }), - openId4VcVerifier: new OpenId4VcVerifierModule({ - verifierMetadata: { - verifierBaseUrl: 'http://localhost:4000', - verificationEndpointPath: '/verify', - }, - }), - } as const -} +export class Verifier extends BaseAgent<{ askar: AskarModule; openId4VcVerifier: OpenId4VcVerifierModule }> { + public verifierRecord!: OpenId4VcVerifierRecord -export class Verifier extends BaseAgent> { public constructor(port: number, name: string) { - super({ port, name, modules: getOpenIdVerifierModules() }) + const openId4VcSiopRouter = Router() + + super({ + port, + name, + modules: { + askar: new AskarModule({ ariesAskar }), + openId4VcVerifier: new OpenId4VcVerifierModule({ + baseUrl: 'http://localhost:4000/siop', + }), + }, + }) + + this.app.use('/siop', openId4VcSiopRouter) } public static async build(): Promise { const verifier = new Verifier(4000, 'OpenId4VcVerifier ' + Math.random().toString()) await verifier.initializeAgent('96213c3d7fc8d4d6754c7a0fd969598g') + verifier.verifierRecord = await verifier.agent.modules.openId4VcVerifier.createVerifier() return verifier } - public async configureVerifierRouter(): Promise { - const endpointConfig: VerifierEndpointConfig = { - basePath: '/', - verificationEndpointConfig: { - enabled: true, - proofResponseHandler: Verifier.proofResponseHandler, + // TODO: add method to show the received presentation submission + public async createProofRequest(presentationDefinition: DifPresentationExchangeDefinitionV2) { + const { authorizationRequestUri } = await this.agent.modules.openId4VcVerifier.createAuthorizationRequest({ + requestSigner: { + method: 'did', + didUrl: this.verificationMethod.id, }, - } - - const router = await this.agent.modules.openId4VcVerifier.configureRouter(Router(), endpointConfig) - this.app.use('/', router) - return router - } - - public async createProofRequest(presentationDefinition: PresentationDefinitionV2) { - const createProofRequestOptions: CreateProofRequestOptions = { - verificationMethod: this.verificationMethod, - presentationDefinition, - holderMetadata: { - ...staticOpOpenIdConfig, - idTokenSigningAlgValuesSupported: [SigningAlgo.EDDSA], - requestObjectSigningAlgValuesSupported: [SigningAlgo.EDDSA], - vpFormatsSupported: { jwt_vc: { alg: [SigningAlgo.EDDSA] }, jwt_vp: { alg: [SigningAlgo.EDDSA] } }, + verifierId: this.verifierRecord.verifierId, + presentationExchange: { + definition: presentationDefinition, }, - } - - const { proofRequest } = await this.agent.modules.openId4VcVerifier.createProofRequest(createProofRequestOptions) - - return proofRequest - } + }) - private static proofResponseHandler: ProofResponseHandler = async (payload) => { - console.log('Received a valid proof response', payload) - return { status: 200 } + return authorizationRequestUri } public async exit() { diff --git a/demo-openid/src/VerifierInquirer.ts b/demo-openid/src/VerifierInquirer.ts index 46de1521cd..8877242fb6 100644 --- a/demo-openid/src/VerifierInquirer.ts +++ b/demo-openid/src/VerifierInquirer.ts @@ -31,7 +31,6 @@ export class VerifierInquirer extends BaseInquirer { public static async build(): Promise { const verifier = await Verifier.build() - await verifier.configureVerifierRouter() return new VerifierInquirer(verifier) } diff --git a/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiableCredential.ts b/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiableCredential.ts index 740c639472..c0a281a7ba 100644 --- a/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiableCredential.ts +++ b/packages/core/src/modules/vc/data-integrity/models/W3cJsonLdVerifiableCredential.ts @@ -1,5 +1,6 @@ import type { LinkedDataProofOptions } from './LinkedDataProof' import type { W3cCredentialOptions } from '../../models/credential/W3cCredential' +import type { W3cJsonCredential } from '../../models/credential/W3cJsonCredential' import { ValidateNested } from 'class-validator' @@ -59,4 +60,8 @@ export class W3cJsonLdVerifiableCredential extends W3cCredential { public get encoded() { return this.toJson() } + + public get jsonCredential(): W3cJsonCredential { + return this.toJson() as W3cJsonCredential + } } diff --git a/packages/core/src/modules/vc/jwt-vc/W3cJwtVerifiableCredential.ts b/packages/core/src/modules/vc/jwt-vc/W3cJwtVerifiableCredential.ts index c9d3852a35..869f00121e 100644 --- a/packages/core/src/modules/vc/jwt-vc/W3cJwtVerifiableCredential.ts +++ b/packages/core/src/modules/vc/jwt-vc/W3cJwtVerifiableCredential.ts @@ -1,6 +1,8 @@ import type { W3cCredential } from '../models/credential/W3cCredential' +import type { W3cJsonCredential } from '../models/credential/W3cJsonCredential' import { Jwt } from '../../../crypto/jose/jwt/Jwt' +import { JsonTransformer } from '../../../utils' import { ClaimFormat } from '../models/ClaimFormat' import { getCredentialFromJwtPayload } from './credentialTransformer' @@ -117,4 +119,8 @@ export class W3cJwtVerifiableCredential { public get encoded() { return this.serializedJwt } + + public get jsonCredential(): W3cJsonCredential { + return JsonTransformer.toJSON(this.credential) as W3cJsonCredential + } } diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts index 23d5104e57..fb8ebd25b1 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts @@ -90,7 +90,7 @@ export class OpenId4VcSiopHolderService { let presentationExchangeOptions: PresentationExchangeResponseOpts | undefined = undefined // Handle presentation exchange part - if (authorizationRequest.presentationDefinitions) { + if (authorizationRequest.presentationDefinitions && authorizationRequest.presentationDefinitions.length > 0) { if (!presentationExchange) { throw new AriesFrameworkError( 'Authorization request included presentation definition. `presentationExchange` MUST be supplied to accept authorization requests.' @@ -127,7 +127,7 @@ export class OpenId4VcSiopHolderService { } } else if (options.presentationExchange) { throw new AriesFrameworkError( - '`presentationExchange` was supplied, but no presentation definition was found in the presentaiton request.' + '`presentationExchange` was supplied, but no presentation definition was found in the presentation request.' ) } @@ -159,9 +159,21 @@ export class OpenId4VcSiopHolderService { ) const response = await openidProvider.submitAuthorizationResponse(authorizationResponseWithCorrelationId) + let responseDetails: string | Record | undefined = undefined + try { + responseDetails = await response.text() + if (responseDetails.includes('{')) { + responseDetails = JSON.parse(responseDetails) + } + } catch (error) { + // no-op + } + return { - ok: response.status === 200, - status: response.status, + serverResponse: { + status: response.status, + body: responseDetails, + }, submittedResponse: authorizationResponseWithCorrelationId.response.payload, } } diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/openId4vc-holder-module.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/OpenId4VcHolderModule.test.ts similarity index 100% rename from packages/openid4vc/src/openid4vc-holder/__tests__/openId4vc-holder-module.test.ts rename to packages/openid4vc/src/openid4vc-holder/__tests__/OpenId4VcHolderModule.test.ts diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts index a98fa20082..83c7a2b0cc 100644 --- a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vci-holder.e2e.test.ts @@ -101,7 +101,7 @@ describe('OpenId4VcHolder', () => { // We only allow EdDSa, as we've created a did with keyType ed25519. If we create // or determine the did dynamically we could use any signature algorithm allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - credentialsToRequest: resolved.offeredCredentials.filter((c) => c.format === 'jwt_vc_json'), + credentialsToRequest: resolved.offeredCredentials.filter((c) => c.format === 'jwt_vc_json').map((m) => m.id), credentialBindingResolver: () => ({ method: 'did', didUrl: holderVerificationMethod }), }) @@ -144,7 +144,7 @@ describe('OpenId4VcHolder', () => { // We only allow EdDSa, as we've created a did with keyType ed25519. If we create // or determine the did dynamically we could use any signature algorithm allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - credentialsToRequest: resolved.offeredCredentials.filter((c) => c.format === 'jwt_vc_json'), + credentialsToRequest: resolved.offeredCredentials.filter((c) => c.format === 'jwt_vc_json').map((m) => m.id), credentialBindingResolver: () => ({ method: 'did', didUrl: holderVerificationMethod }), }) ) @@ -181,7 +181,7 @@ describe('OpenId4VcHolder', () => { // We only allow EdDSa, as we've created a did with keyType ed25519. If we create // or determine the did dynamically we could use any signature algorithm allowedProofOfPossessionSignatureAlgorithms: [JwaSignatureAlgorithm.EdDSA], - credentialsToRequest: resolved.offeredCredentials.filter((c) => c.format === 'vc+sd-jwt'), + credentialsToRequest: resolved.offeredCredentials.filter((c) => c.format === 'vc+sd-jwt').map((m) => m.id), credentialBindingResolver: () => ({ method: 'jwk', jwk: getJwkFromKey(holderKey) }), }) diff --git a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts index 6c7b4d4166..8b511cc99a 100644 --- a/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-holder/__tests__/openid4vp-holder.e2e.test.ts @@ -1,27 +1,14 @@ import type { AgentType } from '../../../tests/utils' -import type { OpenId4VcSiopCreateAuthorizationRequestOptions } from '../../openid4vc-verifier' +import type { OpenId4VcVerifierRecord } from '../../openid4vc-verifier/repository' import type { Express } from 'express' import type { Server } from 'http' -import { AskarModule } from '@aries-framework/askar' -import { W3cJwtVerifiableCredential } from '@aries-framework/core' -import { ariesAskar } from '@hyperledger/aries-askar-nodejs' -import express, { Router } from 'express' -import nock from 'nock' +import express from 'express' import { OpenId4VcHolderModule } from '..' +import { AskarModule } from '../../../../askar/src' +import { askarModuleConfig } from '../../../../askar/tests/helpers' import { createAgentFromModules } from '../../../tests/utils' -import { - openBadgeCredentialPresentationDefinitionLdpVc, - combinePresentationDefinitions, - getOpenBadgeCredentialLdpVc, - openBadgePresentationDefinition, - staticOpOpenIdConfigEdDSA, - universityDegreePresentationDefinition, - waitForMockFunction, - waltPortalOpenBadgeJwt, - waltUniversityDegreeJwt, -} from '../../../tests/utilsVp' import { OpenId4VcVerifierModule } from '../../openid4vc-verifier' const port = 3121 @@ -30,45 +17,37 @@ const verifierBaseUrl = `http://localhost:${port}` const holderModules = { openId4VcHolder: new OpenId4VcHolderModule(), - askar: new AskarModule({ ariesAskar }), + askar: new AskarModule(askarModuleConfig), } const verifierModules = { openId4VcVerifier: new OpenId4VcVerifierModule({ - verifierMetadata: { - verifierBaseUrl: verifierBaseUrl, - verificationEndpointPath, + baseUrl: verifierBaseUrl, + endpoints: { + authorization: { + endpointPath: verificationEndpointPath, + }, }, }), - - askar: new AskarModule({ ariesAskar }), + askar: new AskarModule(askarModuleConfig), } describe('OpenId4VcHolder | OpenID4VP', () => { + let openIdVerifier: OpenId4VcVerifierRecord let verifier: AgentType let holder: AgentType let verifierApp: Express + // eslint-disable-next-line @typescript-eslint/no-explicit-any let verifierServer: Server - const mockFunction = jest.fn() - mockFunction.mockReturnValue({ status: 200 }) - beforeEach(async () => { verifier = await createAgentFromModules('verifier', verifierModules, '96213c3d7fc8d4d6754c7a0fd969598f') + openIdVerifier = await verifier.agent.modules.openId4VcVerifier.createVerifier() holder = await createAgentFromModules('holder', holderModules, '96213c3d7fc8d4d6754c7a0fd969598e') verifierApp = express() - const router = await verifier.agent.modules.openId4VcVerifier.configureRouter(Router(), { - basePath: '/', - verificationEndpointConfig: { - enabled: true, - proofResponseHandler: mockFunction, - }, - }) - - verifierApp.use('/', router) - + verifierApp.use('/', verifier.agent.modules.openId4VcVerifier.config.router) verifierServer = verifierApp.listen(port) }) @@ -80,553 +59,52 @@ describe('OpenId4VcHolder | OpenID4VP', () => { await verifier.agent.wallet.delete() }) - it('siop request with static metadata', async () => { - const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { - verificationMethod: verifier.verificationMethod, - openIdProvider: staticOpOpenIdConfigEdDSA, - } - - //////////////////////////// RP (create request) //////////////////////////// - const { authorizationRequestUri, metadata } = - await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest(createProofRequestOptions) - - //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) - - //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// - - if (result.proofType == 'presentation') throw new Error('Expected an authenticationRequest') - - //////////////////////////// OP (accept the verified request) //////////////////////////// - const { submittedResponse, status } = await holder.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest( - result.authenticationRequest, - holder.verificationMethod - ) - - expect(status).toBe(200) - - expect(result.authenticationRequest.authorizationRequestPayload.redirect_uri).toBe( - verifierBaseUrl + verificationEndpointPath - ) - expect(result.authenticationRequest.issuer).toBe(verifier.verificationMethod.controller) - - //////////////////////////// RP (verify the response) //////////////////////////// - - const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyAuthorizationResponse( - submittedResponse - ) - - const { state, nonce } = metadata - expect(submission).toBe(undefined) - expect(idTokenPayload).toBeDefined() - expect(idTokenPayload.state).toMatch(state) - expect(idTokenPayload.nonce).toMatch(nonce) - - await waitForMockFunction(mockFunction) - expect(mockFunction).toBeCalledWith({ - idTokenPayload: expect.objectContaining(idTokenPayload), - submission: undefined, - }) - }) - - // TODO: not working yet - xit('siop request with issuer', async () => { - nock('https://helloworld.com') - .get('/.well-known/openid-configuration') - .reply(200, staticOpOpenIdConfigEdDSA) - .get('/.well-known/openid-configuration') - .reply(200, staticOpOpenIdConfigEdDSA) - .get('/.well-known/openid-configuration') - .reply(200, staticOpOpenIdConfigEdDSA) - .get('/.well-known/openid-configuration') - .reply(200, staticOpOpenIdConfigEdDSA) - - const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { - verificationMethod: verifier.verificationMethod, - // TODO: if provided this way client metadata is not resolved for the verification method - openIdProvider: 'https://helloworld.com', - } - - //////////////////////////// RP (create request) //////////////////////////// - const { authorizationRequestUri, metadata } = - await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest(createProofRequestOptions) - - //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) - - //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// - - if (result.proofType == 'presentation') throw new Error('Expected a proofType') - - //////////////////////////// OP (accept the verified request) //////////////////////////// - const { submittedResponse, status } = await holder.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest( - result.authenticationRequest, - holder.verificationMethod - ) - - expect(status).toBe(200) - - //////////////////////////// RP (verify the response) //////////////////////////// - - const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyAuthorizationResponse( - submittedResponse - ) - - const { state, nonce } = metadata - expect(idTokenPayload).toBeDefined() - expect(idTokenPayload.state).toMatch(state) - expect(idTokenPayload.nonce).toMatch(nonce) - - await waitForMockFunction(mockFunction) - expect(mockFunction).toBeCalledWith({ - idTokenPayload: expect.objectContaining(idTokenPayload), - submission: expect.objectContaining(submission), - }) - }) - - it('resolving vp request with no credentials', async () => { - const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { - verificationMethod: verifier.verificationMethod, - openIdProvider: staticOpOpenIdConfigEdDSA, - presentationDefinition: openBadgePresentationDefinition, - } - - const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest( - createProofRequestOptions - ) - - //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) - if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') - - expect(result.credentialsForRequest.areRequirementsSatisfied).toBeFalsy() - expect(result.credentialsForRequest.requirements.length).toBe(1) - }) - - it('resolving vp request with wrong credentials errors', async () => { - await holder.agent.w3cCredentials.storeCredential({ - credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), + it('siop authorization request without presentation exchange', async () => { + const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ + requestSigner: { + method: 'did', + didUrl: verifier.kid, + }, + verifierId: openIdVerifier.verifierId, }) - const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { - verificationMethod: verifier.verificationMethod, - openIdProvider: staticOpOpenIdConfigEdDSA, - presentationDefinition: openBadgePresentationDefinition, - } - - const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest( - createProofRequestOptions + const resolvedAuthorizationRequest = await holder.agent.modules.openId4VcHolder.resolveSiopAuthorizationRequest( + authorizationRequestUri ) - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) - if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') - - //////////////////////////// OP (validate and parse the request) //////////////////////////// - expect(result.credentialsForRequest.areRequirementsSatisfied).toBeFalsy() - expect(result.credentialsForRequest.requirements.length).toBe(1) - }) - - it('expect submitting a wrong submission to fail', async () => { - await holder.agent.w3cCredentials.storeCredential({ - credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), - }) - - await holder.agent.w3cCredentials.storeCredential({ - credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), - }) - - const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { - verificationMethod: verifier.verificationMethod, - openIdProvider: staticOpOpenIdConfigEdDSA, - presentationDefinition: openBadgePresentationDefinition, - } - - const { authorizationRequestUri: openBadge } = - await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest(createProofRequestOptions) - const { authorizationRequestUri: university } = - await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ - ...createProofRequestOptions, - presentationDefinition: universityDegreePresentationDefinition, + const { submittedResponse, serverResponse } = + await holder.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedAuthorizationRequest.authorizationRequest, + // When no VP is created, we need to provide the did we want to use for authentication + openIdTokenIssuer: { + method: 'did', + didUrl: holder.kid, + }, }) - //////////////////////////// OP (validate and parse the request) //////////////////////////// - - const resolvedOpenBadge = await holder.agent.modules.openId4VcHolder.resolveProofRequest(openBadge) - const resolvedUniversityDegree = await holder.agent.modules.openId4VcHolder.resolveProofRequest(university) - if (resolvedOpenBadge.proofType !== 'presentation') throw new Error('expected prooftype presentation') - if (resolvedUniversityDegree.proofType !== 'presentation') throw new Error('expected prooftype presentation') - - await expect( - holder.agent.modules.openId4VcHolder.acceptPresentationRequest(resolvedOpenBadge.presentationRequest, { - submission: resolvedUniversityDegree.credentialsForRequest, - submissionEntryIndexes: [0], - }) - ).rejects.toThrow() - }) - - it('resolving vp request with multiple credentials in wallet only allows selecting the correct ones', async () => { - await holder.agent.w3cCredentials.storeCredential({ - credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), - }) - - await holder.agent.w3cCredentials.storeCredential({ - credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), - }) - - const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { - verificationMethod: verifier.verificationMethod, - openIdProvider: staticOpOpenIdConfigEdDSA, - presentationDefinition: openBadgePresentationDefinition, - } - - const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest( - createProofRequestOptions - ) - - //////////////////////////// OP (validate and parse the request) //////////////////////////// - - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) - if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') - - const { presentationRequest, credentialsForRequest } = result - expect(credentialsForRequest.areRequirementsSatisfied).toBeTruthy() - expect(credentialsForRequest.requirements.length).toBe(1) - expect(credentialsForRequest.requirements[0].needsCount).toBe(1) - expect(credentialsForRequest.requirements[0].submissionEntry.length).toBe(1) - expect(credentialsForRequest.requirements[0].submissionEntry[0].inputDescriptorId).toBe('OpenBadgeCredential') - - expect(presentationRequest.presentationDefinitions[0].definition).toMatchObject(openBadgePresentationDefinition) - }) - - it('resolving vp request with multiple credentials in wallet select the correct credentials from the wallet', async () => { - await holder.agent.w3cCredentials.storeCredential({ - credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), - }) - - await holder.agent.w3cCredentials.storeCredential({ - credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), - }) - - const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { - verificationMethod: verifier.verificationMethod, - openIdProvider: staticOpOpenIdConfigEdDSA, - presentationDefinition: combinePresentationDefinitions([ - openBadgePresentationDefinition, - universityDegreePresentationDefinition, - ]), - } - - const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest( - createProofRequestOptions - ) - - //////////////////////////// OP (validate and parse the request) //////////////////////////// - - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) - if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') - - const { credentialsForRequest } = result - expect(credentialsForRequest.areRequirementsSatisfied).toBeTruthy() - expect(credentialsForRequest.requirements.length).toBe(2) - expect(credentialsForRequest.requirements[0].needsCount).toBe(1) - expect(credentialsForRequest.requirements[0].submissionEntry.length).toBe(1) - expect(credentialsForRequest.requirements[1].needsCount).toBe(1) - expect(credentialsForRequest.requirements[1].submissionEntry.length).toBe(1) - expect(credentialsForRequest.requirements[0].submissionEntry[0].inputDescriptorId).toBe('OpenBadgeCredential') - expect(credentialsForRequest.requirements[1].submissionEntry[0].inputDescriptorId).toBe('UniversityDegree') - - const { submittedResponse, status } = await holder.agent.modules.openId4VcHolder.acceptPresentationRequest( - result.presentationRequest, - { - submission: result.credentialsForRequest, - submissionEntryIndexes: [0, 0], - } - ) - - expect(status).toBe(200) - - const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyAuthorizationResponse( - submittedResponse - ) - - expect(idTokenPayload).toBeDefined() - expect(submission).toBeDefined() - expect(submission?.presentationDefinitions).toHaveLength(1) - expect(submission?.submissionData.definition_id).toBe('Combined') - expect(submission?.presentations).toHaveLength(1) - expect(submission?.presentations[0].vcs).toHaveLength(2) - - if (submission?.presentations[0].vcs[0].credential.type.includes('OpenBadgeCredential')) { - expect(submission?.presentations[0].vcs[0].credential.type).toEqual([ - 'VerifiableCredential', - 'OpenBadgeCredential', - ]) - expect(submission?.presentations[0].vcs[1].credential.type).toEqual([ - 'VerifiableCredential', - 'UniversityDegreeCredential', - ]) - } else { - expect(submission?.presentations[0].vcs[1].credential.type).toEqual([ - 'VerifiableCredential', - 'OpenBadgeCredential', - ]) - expect(submission?.presentations[0].vcs[0].credential.type).toEqual([ - 'VerifiableCredential', - 'UniversityDegreeCredential', - ]) - } - - await waitForMockFunction(mockFunction) - expect(mockFunction).toBeCalledWith({ - idTokenPayload: expect.objectContaining(idTokenPayload), - submission: expect.objectContaining(submission), - }) - }) - - it('expect accepting a proof request with only a partial set of requirements to error', async () => { - await holder.agent.w3cCredentials.storeCredential({ - credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), + expect(serverResponse).toEqual({ + status: 200, + body: '', }) - await holder.agent.w3cCredentials.storeCredential({ - credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), + expect(submittedResponse).toMatchObject({ + expires_in: 6000, + id_token: expect.any(String), + state: expect.any(String), }) - const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { - verificationMethod: verifier.verificationMethod, - openIdProvider: staticOpOpenIdConfigEdDSA, - presentationDefinition: combinePresentationDefinitions([ - openBadgePresentationDefinition, - universityDegreePresentationDefinition, - ]), - } - - const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest( - createProofRequestOptions - ) - - //////////////////////////// OP (validate and parse the request) //////////////////////////// - - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) - if (result.proofType !== 'presentation') throw new Error('expected prooftype presentation') - - await expect( - holder.agent.modules.openId4VcHolder.acceptPresentationRequest(result.presentationRequest, { - submission: result.credentialsForRequest, - submissionEntryIndexes: [0], + const { idToken, presentationExchange } = + await verifier.agent.modules.openId4VcVerifier.verifyAuthorizationResponse({ + authorizationResponse: submittedResponse, + verifierId: openIdVerifier.verifierId, }) - ).rejects.toThrow() - }) - - it('expect vp request with single requested credential to succeed', async () => { - await holder.agent.w3cCredentials.storeCredential({ - credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt), - }) - - const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { - verificationMethod: verifier.verificationMethod, - openIdProvider: staticOpOpenIdConfigEdDSA, - presentationDefinition: openBadgePresentationDefinition, - } - - const { authorizationRequestUri, metadata } = - await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest(createProofRequestOptions) - - //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) - if (result.proofType === 'authentication') throw new Error('Expected a proofRequest') - - //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// - // Select the appropriate credentials - - result.credentialsForRequest.requirements[0] - - if (!result.credentialsForRequest.areRequirementsSatisfied) { - throw new Error('Requirements are not satisfied.') - } - - //////////////////////////// OP (accept the verified request) //////////////////////////// - const { submittedResponse, status } = await holder.agent.modules.openId4VcHolder.acceptPresentationRequest( - result.presentationRequest, - { - submission: result.credentialsForRequest, - submissionEntryIndexes: [0], - } - ) - - expect(status).toBe(200) - - // The RP MUST validate that the aud (audience) Claim contains the value of the client_id - // that the RP sent in the Authorization Request as an audience. - // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. - const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyAuthorizationResponse( - submittedResponse - ) - - const { state, nonce } = metadata - expect(idTokenPayload).toBeDefined() - expect(idTokenPayload.state).toMatch(state) - expect(idTokenPayload.nonce).toMatch(nonce) - expect(submission).toBeDefined() - expect(submission?.presentationDefinitions).toHaveLength(1) - expect(submission?.submissionData.definition_id).toBe('OpenBadgeCredential') - expect(submission?.presentations).toHaveLength(1) - expect(submission?.presentations[0].vcs[0].credential.type).toEqual(['VerifiableCredential', 'OpenBadgeCredential']) - - await waitForMockFunction(mockFunction) - expect(mockFunction).toBeCalledWith({ - idTokenPayload: expect.objectContaining(idTokenPayload), - submission: expect.objectContaining(submission), - }) - }) - - it('expect vp request with single requested ldp_vc credential to succeed', async () => { - const credential = await getOpenBadgeCredentialLdpVc( - verifier.agent.context, - verifier.verificationMethod, - holder.verificationMethod - ) - await holder.agent.w3cCredentials.storeCredential({ - credential, - }) - - const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { - verificationMethod: verifier.verificationMethod, - openIdProvider: staticOpOpenIdConfigEdDSA, - presentationDefinition: openBadgeCredentialPresentationDefinitionLdpVc, - } - - const { authorizationRequestUri, metadata } = - await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest(createProofRequestOptions) - - //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) - if (result.proofType === 'authentication') throw new Error('Expected a proofRequest') - - //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// - // Select the appropriate credentials - - if (!result.credentialsForRequest.areRequirementsSatisfied) { - throw new Error('Requirements are not satisfied.') - } - - //////////////////////////// OP (accept the verified request) //////////////////////////// - const { submittedResponse, status } = await holder.agent.modules.openId4VcHolder.acceptPresentationRequest( - result.presentationRequest, - { - submission: result.credentialsForRequest, - submissionEntryIndexes: [0], - } - ) - - expect(status).toBe(200) - - // The RP MUST validate that the aud (audience) Claim contains the value of the client_id - // that the RP sent in the Authorization Request as an audience. - // When the request has been signed, the value might be an HTTPS URL, or a Decentralized Identifier. - const { idTokenPayload, submission } = await verifier.agent.modules.openId4VcVerifier.verifyAuthorizationResponse( - submittedResponse - ) - - const { state, nonce } = metadata - expect(idTokenPayload).toBeDefined() - expect(idTokenPayload.state).toMatch(state) - expect(idTokenPayload.nonce).toMatch(nonce) - - expect(submission).toBeDefined() - expect(submission?.presentationDefinitions).toHaveLength(1) - expect(submission?.submissionData.definition_id).toBe('OpenBadgeCredential') - expect(submission?.presentations).toHaveLength(1) - expect(submission?.presentations[0].vcs[0].credential.type).toEqual(['VerifiableCredential', 'OpenBadgeCredential']) - - await waitForMockFunction(mockFunction) - expect(mockFunction).toBeCalledWith({ - idTokenPayload: expect.objectContaining(idTokenPayload), - submission: expect.objectContaining(submission), - }) - }) - - it('expects the submission to fail if there are too few submission entry indexes, and also to fail when requesting two different presentation formats', async () => { - const credential = await getOpenBadgeCredentialLdpVc( - verifier.agent.context, - verifier.verificationMethod, - holder.verificationMethod - ) - - await holder.agent.w3cCredentials.storeCredential({ credential }) - - await holder.agent.w3cCredentials.storeCredential({ - credential: W3cJwtVerifiableCredential.fromSerializedJwt(waltUniversityDegreeJwt), + expect(presentationExchange).toBeUndefined() + expect(idToken).toMatchObject({ + payload: { + state: expect.any(String), + nonce: expect.any(String), + }, }) - - const createProofRequestOptions: OpenId4VcSiopCreateAuthorizationRequestOptions = { - verificationMethod: verifier.verificationMethod, - openIdProvider: staticOpOpenIdConfigEdDSA, - presentationDefinition: combinePresentationDefinitions([ - universityDegreePresentationDefinition, - openBadgeCredentialPresentationDefinitionLdpVc, - ]), - } - - const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest( - createProofRequestOptions - ) - - //////////////////////////// OP (validate and parse the request) //////////////////////////// - const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) - if (result.proofType === 'authentication') throw new Error('Expected a proofRequest') - - //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// - // Select the appropriate credentials - - if (!result.credentialsForRequest.areRequirementsSatisfied) { - throw new Error('Requirements are not satisfied.') - } - - //////////////////////////// OP (accept the verified request) //////////////////////////// - await expect( - holder.agent.modules.openId4VcHolder.acceptPresentationRequest(result.presentationRequest, { - submission: result.credentialsForRequest, - submissionEntryIndexes: [0, 0], - }) - ).rejects.toThrow() - - await expect( - holder.agent.modules.openId4VcHolder.acceptPresentationRequest(result.presentationRequest, { - submission: result.credentialsForRequest, - submissionEntryIndexes: [0], - }) - ).rejects.toThrow() }) - - // it('edited walt vp request', async () => { - // const credential = W3cJwtVerifiableCredential.fromSerializedJwt(waltPortalOpenBadgeJwt) - // await holder.w3cCredentials.storeCredential({ credential }) - - // const authorizationRequestUri = - // 'openid4vp://authorize?response_type=vp_token&client_id=https%3A%2F%2Fverifier.portal.walt.id%2Fopenid4vc%2Fverify&response_mode=direct_post&state=97509d5c-2dd2-490b-8617-577f45e3b6d0&presentation_definition=%7B%22id%22%3A%22test%22%2C%22input_descriptors%22%3A%5B%7B%22id%22%3A%22OpenBadgeCredential%22%2C%22format%22%3A%7B%22jwt_vc%22%3A%7B%22alg%22%3A%5B%22EdDSA%22%5D%7D%7D%2C%22constraints%22%3A%7B%22fields%22%3A%5B%7B%22path%22%3A%5B%22%24.vc.type.%2A%22%5D%2C%22filter%22%3A%7B%22type%22%3A%22string%22%2C%22pattern%22%3A%22OpenBadgeCredential%22%7D%7D%5D%7D%7D%5D%7D&client_id_scheme=redirect_uri&response_uri=https%3A%2F%2Fverifier.portal.walt.id%2Fopenid4vc%2Fverify%2F97509d5c-2dd2-490b-8617-577f45e3b6d0' - - // //////////////////////////// OP (validate and parse the request) //////////////////////////// - // const result = await holder.agent.modules.openId4VcHolder.resolveProofRequest(authorizationRequestUri) - // if (result.proofType === 'authentication') throw new Error('Expected a proofRequest') - - // //////////////////////////// User (decide wheather or not to accept the request) //////////////////////////// - // // Select the appropriate credentials - - // const { presentationRequest, selectResults } = result - // result.selectResults.requirements[0] - - // if (!result.selectResults.areRequirementsSatisfied) { - // throw new Error('Requirements are not satisfied.') - // } - - // //////////////////////////// OP (accept the verified request) //////////////////////////// - // const responseStatus = await holder.agent.modules.openId4VcHolder.acceptPresentationRequest(presentationRequest, { - // submission: selectResults, - // submissionEntryIndexes: [0], - // }) - - // expect(responseStatus.ok).toBeTruthy() - // }) }) diff --git a/packages/openid4vc/src/openid4vc-issuer/__tests__/openId4vc-issuer-module.test.ts b/packages/openid4vc/src/openid4vc-issuer/__tests__/OpenId4VcIsserModule.test.ts similarity index 100% rename from packages/openid4vc/src/openid4vc-issuer/__tests__/openId4vc-issuer-module.test.ts rename to packages/openid4vc/src/openid4vc-issuer/__tests__/OpenId4VcIsserModule.test.ts diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts index 4d04802de9..4d454231fe 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcSiopVerifierService.ts @@ -135,7 +135,7 @@ export class OpenId4VcSiopVerifierService { } const relyingParty = await this.getRelyingParty(agentContext, options.verifier, { - presentationDefinition: presentationDefinitionsWithLocation?.[0].definition, + presentationDefinition: presentationDefinitionsWithLocation?.[0]?.definition, clientId: requestClientId, }) @@ -156,10 +156,10 @@ export class OpenId4VcSiopVerifierService { }, }) - const presentationExchange = response.oid4vpSubmission + const presentationExchange = response.oid4vpSubmission?.submissionData ? { submission: response.oid4vpSubmission.submissionData, - definition: response.oid4vpSubmission.presentationDefinitions[0].definition, + definition: response.oid4vpSubmission.presentationDefinitions[0]?.definition, presentations: response.oid4vpSubmission?.presentations.map(getVerifiablePresentationFromSphereonWrapped), } : undefined diff --git a/packages/openid4vc/src/openid4vc-verifier/__tests__/openId4vc-verifier-module.test.ts b/packages/openid4vc/src/openid4vc-verifier/__tests__/OpenId4VcVerifierModule.test.ts similarity index 56% rename from packages/openid4vc/src/openid4vc-verifier/__tests__/openId4vc-verifier-module.test.ts rename to packages/openid4vc/src/openid4vc-verifier/__tests__/OpenId4VcVerifierModule.test.ts index cf9becd453..4251c0586e 100644 --- a/packages/openid4vc/src/openid4vc-verifier/__tests__/openId4vc-verifier-module.test.ts +++ b/packages/openid4vc/src/openid4vc-verifier/__tests__/OpenId4VcVerifierModule.test.ts @@ -1,9 +1,13 @@ -/* eslint-disable @typescript-eslint/unbound-method */ +import type { OpenId4VcVerifierModuleConfigOptions } from '../OpenId4VcVerifierModuleConfig' import type { DependencyManager } from '@aries-framework/core' +import { Router } from 'express' + +import { OpenId4VcSiopVerifierService } from '../OpenId4VcSiopVerifierService' import { OpenId4VcVerifierApi } from '../OpenId4VcVerifierApi' import { OpenId4VcVerifierModule } from '../OpenId4VcVerifierModule' -import { OpenId4VcSiopVerifierService } from '../OpenId4VcSiopVerifierService' +import { OpenId4VcVerifierModuleConfig } from '../OpenId4VcVerifierModuleConfig' +import { OpenId4VcVerifierRepository } from '../repository' const dependencyManager = { registerInstance: jest.fn(), @@ -14,20 +18,29 @@ const dependencyManager = { describe('OpenId4VcVerifierModule', () => { test('registers dependencies on the dependency manager', () => { - const verifierMetadata = { - verifierBaseUrl: 'http://redirect-uri', - verificationEndpointPath: '', - } - const openId4VcClientModule = new OpenId4VcVerifierModule({ verifierMetadata }) - + const options = { + baseUrl: 'http://localhost:3000', + endpoints: { + authorization: { + endpointPath: '/hello', + }, + }, + router: Router(), + } satisfies OpenId4VcVerifierModuleConfigOptions + const openId4VcClientModule = new OpenId4VcVerifierModule(options) openId4VcClientModule.register(dependencyManager) expect(dependencyManager.registerInstance).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerInstance).toHaveBeenCalledWith( + OpenId4VcVerifierModuleConfig, + new OpenId4VcVerifierModuleConfig(options) + ) expect(dependencyManager.registerContextScoped).toHaveBeenCalledTimes(1) expect(dependencyManager.registerContextScoped).toHaveBeenCalledWith(OpenId4VcVerifierApi) - expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(1) + expect(dependencyManager.registerSingleton).toHaveBeenCalledTimes(2) expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VcSiopVerifierService) + expect(dependencyManager.registerSingleton).toHaveBeenCalledWith(OpenId4VcVerifierRepository) }) }) diff --git a/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.e2e.test.ts b/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.e2e.test.ts index b80d752396..106cce5849 100644 --- a/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.e2e.test.ts +++ b/packages/openid4vc/src/openid4vc-verifier/__tests__/openid4vc-verifier.e2e.test.ts @@ -1,34 +1,25 @@ -import { AskarModule } from '@aries-framework/askar' import { Jwt } from '@aries-framework/core' -import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { SigningAlgo } from '@sphereon/did-auth-siop' import { cleanAll, enableNetConnect } from 'nock' -import { OpenId4VcVerifierModule } from '..' +import { AskarModule } from '../../../../askar/src' +import { askarModuleConfig } from '../../../../askar/tests/helpers' import { createAgentFromModules, type AgentType } from '../../../tests/utils' -import { - staticOpOpenIdConfigEdDSA, - staticSiopConfigEDDSA, - universityDegreePresentationDefinition, -} from '../../../tests/utilsVp' +import { universityDegreePresentationDefinition } from '../../../tests/utilsVp' +import { OpenId4VcVerifierModule } from '../OpenId4VcVerifierModule' const modules = { openId4VcVerifier: new OpenId4VcVerifierModule({ - verifierMetadata: { - verifierBaseUrl: 'http://redirect-uri', - verificationEndpointPath: '', - }, - }), - askar: new AskarModule({ - ariesAskar, + baseUrl: 'http://redirect-uri', }), + askar: new AskarModule(askarModuleConfig), } describe('OpenId4VcVerifier', () => { let verifier: AgentType beforeEach(async () => { - verifier = await createAgentFromModules('verifier', { ...modules }, '96213c3d7fc8d4d6754c7a0fd969598f') + verifier = await createAgentFromModules('verifier', modules, '96213c3d7fc8d4d6754c7a0fd969598f') }) afterEach(async () => { @@ -42,38 +33,33 @@ describe('OpenId4VcVerifier', () => { enableNetConnect() }) - it(`cannot sign authorization request with alg that isn't supported by the OpenId Provider`, async () => { - await expect( - verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ - verificationEndpointUrl: 'http://redirect-uri', - verificationMethod: verifier.verificationMethod, - }) - ).rejects.toThrow() - }) - - it(`check openid proof request format`, async () => { + it('check openid proof request format', async () => { + const openIdVerifier = await verifier.agent.modules.openId4VcVerifier.createVerifier() const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ - verificationEndpointUrl: 'http://redirect-uri', - verificationMethod: verifier.verificationMethod, - openIdProvider: staticOpOpenIdConfigEdDSA, - presentationDefinition: universityDegreePresentationDefinition, + requestSigner: { + method: 'did', + didUrl: verifier.kid, + }, + verifierId: openIdVerifier.verifierId, + presentationExchange: { + definition: universityDegreePresentationDefinition, + }, }) - const base = - 'openid://?redirect_uri=http%3A%2F%2Fredirect-uri&presentation_definition=%7B%22id%22%3A%22UniversityDegreeCredential%22%2C%22input_descriptors%22%3A%5B%7B%22id%22%3A%22UniversityDegree%22%2C%22format%22%3A%7B%22jwt_vc%22%3A%7B%22alg%22%3A%5B%22EdDSA%22%5D%7D%7D%2C%22constraints%22%3A%7B%22fields%22%3A%5B%7B%22path%22%3A%5B%22%24.vc.type.*%22%5D%2C%22filter%22%3A%7B%22type%22%3A%22string%22%2C%22pattern%22%3A%22UniversityDegree%22%7D%7D%5D%7D%7D%5D%7D&request=' + const base = `openid://?redirect_uri=http%3A%2F%2Fredirect-uri%2F${openIdVerifier.verifierId}%2Fauthorize&request=` expect(authorizationRequestUri.startsWith(base)).toBe(true) const _jwt = authorizationRequestUri.substring(base.length) const jwt = Jwt.fromSerializedJwt(_jwt) - expect(authorizationRequestUri.startsWith(base)).toBe(true) - expect(jwt.header.kid).toEqual(verifier.kid) expect(jwt.header.alg).toEqual(SigningAlgo.EDDSA) expect(jwt.header.typ).toEqual('JWT') expect(jwt.payload.additionalClaims.scope).toEqual('openid') - expect(jwt.payload.additionalClaims.client_id).toEqual(verifier.kid) - expect(jwt.payload.additionalClaims.redirect_uri).toEqual('http://redirect-uri') + expect(jwt.payload.additionalClaims.client_id).toEqual(verifier.did) + expect(jwt.payload.additionalClaims.redirect_uri).toEqual( + `http://redirect-uri/${openIdVerifier.verifierId}/authorize` + ) expect(jwt.payload.additionalClaims.response_mode).toEqual('post') expect(jwt.payload.additionalClaims.nonce).toBeDefined() expect(jwt.payload.additionalClaims.state).toBeDefined() @@ -81,32 +67,5 @@ describe('OpenId4VcVerifier', () => { expect(jwt.payload.iss).toEqual(verifier.did) expect(jwt.payload.sub).toEqual(verifier.did) }) - - it(`check siop proof request format`, async () => { - const { authorizationRequestUri } = await verifier.agent.modules.openId4VcVerifier.createAuthorizationRequest({ - verificationEndpointUrl: 'http://redirect-uri', - verificationMethod: verifier.verificationMethod, - openIdProvider: staticSiopConfigEDDSA, - }) - - // TODO: this should be siopv2 - const base = 'openid://?redirect_uri=http%3A%2F%2Fredirect-uri&request=' - expect(authorizationRequestUri.startsWith(base)).toBe(true) - - const _jwt = authorizationRequestUri.substring(base.length) - const jwt = Jwt.fromSerializedJwt(_jwt) - - expect(jwt.header.kid).toEqual(verifier.kid) - expect(jwt.header.alg).toEqual(SigningAlgo.EDDSA) - expect(jwt.payload.additionalClaims.scope).toEqual('openid') - expect(jwt.payload.additionalClaims.client_id).toEqual(verifier.kid) - expect(jwt.payload.additionalClaims.redirect_uri).toEqual('http://redirect-uri') - expect(jwt.payload.additionalClaims.response_mode).toEqual('post') - expect(jwt.payload.additionalClaims.response_type).toEqual('id_token') - expect(jwt.payload.additionalClaims.nonce).toBeDefined() - expect(jwt.payload.additionalClaims.state).toBeDefined() - expect(jwt.payload.iss).toEqual(verifier.did) - expect(jwt.payload.sub).toEqual(verifier.did) - }) }) }) diff --git a/packages/openid4vc/src/openid4vc-verifier/index.ts b/packages/openid4vc/src/openid4vc-verifier/index.ts index 7c8fd8a4b1..25a6548336 100644 --- a/packages/openid4vc/src/openid4vc-verifier/index.ts +++ b/packages/openid4vc/src/openid4vc-verifier/index.ts @@ -3,3 +3,4 @@ export * from './OpenId4VcVerifierModule' export * from './OpenId4VcSiopVerifierService' export * from './OpenId4VcSiopVerifierServiceOptions' export * from './OpenId4VcVerifierModuleConfig' +export * from './repository' diff --git a/packages/openid4vc/tests/openid4vc.e2e.test.ts b/packages/openid4vc/tests/openid4vc.e2e.test.ts index 7039161bb5..038c854c76 100644 --- a/packages/openid4vc/tests/openid4vc.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc.e2e.test.ts @@ -384,7 +384,7 @@ describe('OpenId4Vc', () => { resolvedProofRequest1.presentationExchange.credentialsForRequest ) - const { status: status1, submittedResponse: submittedResponse1 } = + const { submittedResponse: submittedResponse1, serverResponse: serverResponse1 } = await holderTenant.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ authorizationRequest: resolvedProofRequest1.authorizationRequest, presentationExchange: { @@ -414,7 +414,9 @@ describe('OpenId4Vc', () => { state: expect.any(String), vp_token: expect.any(String), }) - expect(status1).toBe(200) + expect(serverResponse1).toMatchObject({ + status: 200, + }) // The RP MUST validate that the aud (audience) Claim contains the value of the client_id // that the RP sent in the Authorization Request as an audience. @@ -452,14 +454,16 @@ describe('OpenId4Vc', () => { resolvedProofRequest2.presentationExchange.credentialsForRequest ) - const { status: status2, submittedResponse: submittedResponse2 } = + const { serverResponse: serverResponse2, submittedResponse: submittedResponse2 } = await holderTenant.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ authorizationRequest: resolvedProofRequest2.authorizationRequest, presentationExchange: { credentials: selectedCredentials2, }, }) - expect(status2).toBe(200) + expect(serverResponse2).toMatchObject({ + status: 200, + }) // The RP MUST validate that the aud (audience) Claim contains the value of the client_id // that the RP sent in the Authorization Request as an audience. @@ -601,12 +605,13 @@ describe('OpenId4Vc', () => { resolvedAuthorizationRequest.presentationExchange.credentialsForRequest ) - const { status, submittedResponse } = await holder.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ - authorizationRequest: resolvedAuthorizationRequest.authorizationRequest, - presentationExchange: { - credentials: selectedCredentials, - }, - }) + const { serverResponse, submittedResponse } = + await holder.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest({ + authorizationRequest: resolvedAuthorizationRequest.authorizationRequest, + presentationExchange: { + credentials: selectedCredentials, + }, + }) // path_nested should not be used for sd-jwt expect(submittedResponse.presentation_submission?.descriptor_map[0].path_nested).toBeUndefined() @@ -627,7 +632,9 @@ describe('OpenId4Vc', () => { state: expect.any(String), vp_token: expect.any(String), }) - expect(status).toBe(200) + expect(serverResponse).toMatchObject({ + status: 200, + }) // The RP MUST validate that the aud (audience) Claim contains the value of the client_id // that the RP sent in the Authorization Request as an audience. diff --git a/tsconfig.test.json b/tsconfig.test.json index 4fa873e5a5..b39f15bce9 100644 --- a/tsconfig.test.json +++ b/tsconfig.test.json @@ -4,6 +4,9 @@ "require": ["tsconfig-paths/register"] }, "compilerOptions": { + // Needed because of type-issued in sphereon siop-oid4vp lib + // https://github.com/Sphereon-Opensource/SIOP-OID4VP/pull/71#issuecomment-1913552869 + "skipLibCheck": true, "baseUrl": ".", "paths": { "@aries-framework/*": ["packages/*/src"] diff --git a/yarn.lock b/yarn.lock index 9262f00d5d..0460bc2a93 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3109,7 +3109,7 @@ dependencies: "@types/express" "*" -"@types/node@*", "@types/node@>=13.7.0", "@types/node@^18.18.8": +"@types/node@*", "@types/node@18.18.8", "@types/node@>=13.7.0", "@types/node@^18.18.8": version "18.18.8" resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.8.tgz#2b285361f2357c8c8578ec86b5d097c7f464cfd6" integrity sha512-OLGBaaK5V3VRBS1bAkMVP2/W9B+H8meUfl866OrMNQqt7wDgdpWPp5o6gmIc9pB+lIQHSq4ZL8ypeH1vPxcPaQ== From b172c3de668a808aa71d3402f960126fa32f2300 Mon Sep 17 00:00:00 2001 From: Timo Glastra Date: Sun, 28 Jan 2024 17:56:52 +0700 Subject: [PATCH 115/115] eslint fixes Signed-off-by: Timo Glastra --- demo/src/BaseAgent.ts | 45 ------------------- packages/anoncreds-rs/tests/anoncredsSetup.ts | 1 + packages/anoncreds/src/models/exchange.ts | 1 + packages/anoncreds/src/utils/credential.ts | 2 +- .../anoncreds/tests/legacyAnonCredsSetup.ts | 2 + .../services/CheqdAnonCredsRegistry.ts | 2 +- .../modules/vc/data-integrity/deriveProof.ts | 1 + .../proof-purposes/ProofPurpose.ts | 1 + .../services/IndySdkVerifierService.ts | 2 +- .../ledger/serializeRequestForSignature.ts | 4 +- packages/openid4vc/package.json | 1 + .../OpenId4VcVerifierModule.ts | 2 +- 12 files changed, 14 insertions(+), 50 deletions(-) diff --git a/demo/src/BaseAgent.ts b/demo/src/BaseAgent.ts index c2e787e32a..e78d35b564 100644 --- a/demo/src/BaseAgent.ts +++ b/demo/src/BaseAgent.ts @@ -32,14 +32,12 @@ import { Agent, HttpOutboundTransport, } from '@aries-framework/core' -import { IndySdkAnonCredsRegistry, IndySdkModule, IndySdkSovDidResolver } from '@aries-framework/indy-sdk' import { IndyVdrIndyDidResolver, IndyVdrAnonCredsRegistry, IndyVdrModule } from '@aries-framework/indy-vdr' import { agentDependencies, HttpInboundTransport } from '@aries-framework/node' import { anoncreds } from '@hyperledger/anoncreds-nodejs' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' import { indyVdr } from '@hyperledger/indy-vdr-nodejs' import { randomUUID } from 'crypto' -import indySdk from 'indy-sdk' import { greenText } from './OutputClass' @@ -167,46 +165,3 @@ function getAskarAnonCredsIndyModules() { }), } as const } - -function getLegacyIndySdkModules() { - const legacyIndyCredentialFormatService = new LegacyIndyCredentialFormatService() - const legacyIndyProofFormatService = new LegacyIndyProofFormatService() - - return { - connections: new ConnectionsModule({ - autoAcceptConnections: true, - }), - credentials: new CredentialsModule({ - autoAcceptCredentials: AutoAcceptCredential.ContentApproved, - credentialProtocols: [ - new V1CredentialProtocol({ - indyCredentialFormat: legacyIndyCredentialFormatService, - }), - new V2CredentialProtocol({ - credentialFormats: [legacyIndyCredentialFormatService], - }), - ], - }), - proofs: new ProofsModule({ - autoAcceptProofs: AutoAcceptProof.ContentApproved, - proofProtocols: [ - new V1ProofProtocol({ - indyProofFormat: legacyIndyProofFormatService, - }), - new V2ProofProtocol({ - proofFormats: [legacyIndyProofFormatService], - }), - ], - }), - anoncreds: new AnonCredsModule({ - registries: [new IndySdkAnonCredsRegistry()], - }), - indySdk: new IndySdkModule({ - indySdk, - networks: [indyNetworkConfig], - }), - dids: new DidsModule({ - resolvers: [new IndySdkSovDidResolver()], - }), - } as const -} diff --git a/packages/anoncreds-rs/tests/anoncredsSetup.ts b/packages/anoncreds-rs/tests/anoncredsSetup.ts index 2ce0a49fee..8e62c72486 100644 --- a/packages/anoncreds-rs/tests/anoncredsSetup.ts +++ b/packages/anoncreds-rs/tests/anoncredsSetup.ts @@ -55,6 +55,7 @@ import { LocalDidResolver } from './LocalDidResolver' // Helper type to get the type of the agents (with the custom modules) for the credential tests export type AnonCredsTestsAgent = Agent< + // eslint-disable-next-line @typescript-eslint/no-explicit-any ReturnType & { mediationRecipient?: any; mediator?: any } > diff --git a/packages/anoncreds/src/models/exchange.ts b/packages/anoncreds/src/models/exchange.ts index 5213153ff9..7d483a602f 100644 --- a/packages/anoncreds/src/models/exchange.ts +++ b/packages/anoncreds/src/models/exchange.ts @@ -90,6 +90,7 @@ export interface AnonCredsProof { predicates: Record } // TODO: extend types for proof property + // eslint-disable-next-line @typescript-eslint/no-explicit-any proof: any identifiers: Array<{ schema_id: string diff --git a/packages/anoncreds/src/utils/credential.ts b/packages/anoncreds/src/utils/credential.ts index 8ae3f54382..c60dbfc6ce 100644 --- a/packages/anoncreds/src/utils/credential.ts +++ b/packages/anoncreds/src/utils/credential.ts @@ -1,7 +1,7 @@ import type { AnonCredsSchema, AnonCredsCredentialValues } from '../models' import type { CredentialPreviewAttributeOptions, LinkedAttachment } from '@aries-framework/core' -import { AriesFrameworkError, Hasher, encodeAttachment, Buffer } from '@aries-framework/core' +import { AriesFrameworkError, Hasher, encodeAttachment } from '@aries-framework/core' import BigNumber from 'bn.js' const isString = (value: unknown): value is string => typeof value === 'string' diff --git a/packages/anoncreds/tests/legacyAnonCredsSetup.ts b/packages/anoncreds/tests/legacyAnonCredsSetup.ts index baef3eac54..b8bc25d359 100644 --- a/packages/anoncreds/tests/legacyAnonCredsSetup.ts +++ b/packages/anoncreds/tests/legacyAnonCredsSetup.ts @@ -74,7 +74,9 @@ import { // Helper type to get the type of the agents (with the custom modules) for the credential tests export type AnonCredsTestsAgent = + // eslint-disable-next-line @typescript-eslint/no-explicit-any | Agent & { mediationRecipient?: any; mediator?: any }> + // eslint-disable-next-line @typescript-eslint/no-explicit-any | Agent & { mediationRecipient?: any; mediator?: any }> export const getLegacyAnonCredsModules = ({ diff --git a/packages/cheqd/src/anoncreds/services/CheqdAnonCredsRegistry.ts b/packages/cheqd/src/anoncreds/services/CheqdAnonCredsRegistry.ts index d6f0049f74..cc4cf60bfd 100644 --- a/packages/cheqd/src/anoncreds/services/CheqdAnonCredsRegistry.ts +++ b/packages/cheqd/src/anoncreds/services/CheqdAnonCredsRegistry.ts @@ -14,7 +14,7 @@ import type { } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' -import { AriesFrameworkError, Buffer, Hasher, JsonTransformer, TypedArrayEncoder, utils } from '@aries-framework/core' +import { AriesFrameworkError, Hasher, JsonTransformer, TypedArrayEncoder, utils } from '@aries-framework/core' import { CheqdDidResolver, CheqdDidRegistrar } from '../../dids' import { cheqdSdkAnonCredsRegistryIdentifierRegex, parseCheqdDid } from '../utils/identifiers' diff --git a/packages/core/src/modules/vc/data-integrity/deriveProof.ts b/packages/core/src/modules/vc/data-integrity/deriveProof.ts index a98bf1a064..78e13826de 100644 --- a/packages/core/src/modules/vc/data-integrity/deriveProof.ts +++ b/packages/core/src/modules/vc/data-integrity/deriveProof.ts @@ -38,6 +38,7 @@ export interface W3cJsonLdDeriveProofOptions { export const deriveProof = async ( proofDocument: JsonObject, revealDocument: JsonObject, + // eslint-disable-next-line @typescript-eslint/no-explicit-any { suite, skipProofCompaction, documentLoader, expansionMap, nonce }: any ): Promise => { if (!suite) { diff --git a/packages/core/src/modules/vc/data-integrity/proof-purposes/ProofPurpose.ts b/packages/core/src/modules/vc/data-integrity/proof-purposes/ProofPurpose.ts index 2695f3276c..af04ec9f41 100644 --- a/packages/core/src/modules/vc/data-integrity/proof-purposes/ProofPurpose.ts +++ b/packages/core/src/modules/vc/data-integrity/proof-purposes/ProofPurpose.ts @@ -1 +1,2 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export type ProofPurpose = any diff --git a/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts b/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts index 80aee7be6f..0f619c8ab8 100644 --- a/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts +++ b/packages/indy-sdk/src/anoncreds/services/IndySdkVerifierService.ts @@ -1,4 +1,4 @@ -import type { AnonCredsProof, AnonCredsVerifierService, VerifyProofOptions } from '@aries-framework/anoncreds' +import type { AnonCredsVerifierService, VerifyProofOptions } from '@aries-framework/anoncreds' import type { AgentContext } from '@aries-framework/core' import type { CredentialDefs, Schemas, RevocRegDefs, RevRegs, IndyProofRequest, IndyProof } from 'indy-sdk' diff --git a/packages/indy-sdk/src/ledger/serializeRequestForSignature.ts b/packages/indy-sdk/src/ledger/serializeRequestForSignature.ts index b29809aa80..55322fc803 100644 --- a/packages/indy-sdk/src/ledger/serializeRequestForSignature.ts +++ b/packages/indy-sdk/src/ledger/serializeRequestForSignature.ts @@ -1,9 +1,10 @@ -import { Hasher, TypedArrayEncoder } from '@aries-framework/core' +import { Hasher } from '@aries-framework/core' const ATTRIB_TYPE = '100' const GET_ATTR_TYPE = '104' /// Generate the normalized form of a ledger transaction request for signing +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function serializeRequestForSignature(v: any): string { const type = v?.operation?.type @@ -17,6 +18,7 @@ export function serializeRequestForSignature(v: any): string { * * @see https://github.com/hyperledger/indy-shared-rs/blob/6af1e939586d1f16341dc03b62970cf28b32d118/indy-utils/src/txn_signature.rs#L10 */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any function _serializeRequestForSignature(v: any, isTopLevel: boolean, _type?: string): string { const vType = typeof v diff --git a/packages/openid4vc/package.json b/packages/openid4vc/package.json index 2ca4ccb45a..8cd74b9926 100644 --- a/packages/openid4vc/package.json +++ b/packages/openid4vc/package.json @@ -32,6 +32,7 @@ "@sphereon/did-auth-siop": "0.6.0-unstable.3" }, "devDependencies": { + "@aries-framework/tenants": "0.4.2", "@types/express": "^4.17.21", "express": "^4.18.2", "nock": "^13.3.0", diff --git a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts index 9ed1b250b3..40a3e644dd 100644 --- a/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts +++ b/packages/openid4vc/src/openid4vc-verifier/OpenId4VcVerifierModule.ts @@ -6,9 +6,9 @@ import { AgentConfig } from '@aries-framework/core' import { getAgentContextForActorId, getRequestContext, importExpress } from '../shared/router' +import { OpenId4VcSiopVerifierService } from './OpenId4VcSiopVerifierService' import { OpenId4VcVerifierApi } from './OpenId4VcVerifierApi' import { OpenId4VcVerifierModuleConfig } from './OpenId4VcVerifierModuleConfig' -import { OpenId4VcSiopVerifierService } from './OpenId4VcSiopVerifierService' import { OpenId4VcVerifierRepository } from './repository' import { configureAuthorizationEndpoint } from './router'