Skip to content

Commit

Permalink
fix(openid4vc): v11 metadata type and transformation (#1983)
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <timo@animo.id>
  • Loading branch information
TimoGlastra committed Aug 3, 2024
1 parent a093150 commit 35a04e3
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 112 deletions.
5 changes: 5 additions & 0 deletions .changeset/heavy-gorillas-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@credo-ts/openid4vc': patch
---

fix v11 metadata typing and update v11<->v13 tranformation logic accordingly
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export class OpenId4VcHolderApi {
* @returns The uniform credential offer payload, the issuer metadata, protocol version, and the offered credentials with metadata.
*/
public async resolveCredentialOffer(credentialOffer: string) {
return await this.openId4VciHolderService.resolveCredentialOffer(credentialOffer)
return await this.openId4VciHolderService.resolveCredentialOffer(this.agentContext, credentialOffer)
}

/**
Expand Down
62 changes: 24 additions & 38 deletions packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import type {
OpenId4VciTokenRequestOptions,
} from './OpenId4VciHolderServiceOptions'
import type {
OpenId4VciCredentialConfigurationsSupported,
OpenId4VciCredentialConfigurationSupported,
OpenId4VciCredentialSupported,
OpenId4VciIssuerMetadata,
Expand All @@ -25,8 +24,6 @@ import type {
OpenIDResponse,
AuthorizationDetails,
AuthorizationDetailsJwtVcJson,
CredentialIssuerMetadataV1_0_11,
CredentialIssuerMetadataV1_0_13,
AuthorizationDetailsJwtVcJsonLdAndLdpVc,
AuthorizationDetailsSdJwtVc,
} from '@sphereon/oid4vci-common'
Expand Down Expand Up @@ -66,12 +63,7 @@ import {
import { CodeChallengeMethod, OpenId4VCIVersion, PARMode, post } from '@sphereon/oid4vci-common'

import { OpenId4VciCredentialFormatProfile } from '../shared'
import {
getTypesFromCredentialSupported,
getOfferedCredentials,
credentialsSupportedV11ToV13,
} from '../shared/issuerMetadataUtils'
import { OpenId4VciCredentialSupportedWithId } from '../shared/models/index'
import { getTypesFromCredentialSupported, getOfferedCredentials } from '../shared/issuerMetadataUtils'
import { getSupportedJwaSignatureAlgorithms, isCredentialOfferV1Draft13 } from '../shared/utils'

import { openId4VciSupportedCredentialFormats, OpenId4VciNotificationMetadata } from './OpenId4VciHolderServiceOptions'
Expand All @@ -92,7 +84,10 @@ export class OpenId4VciHolderService {
this.logger = logger
}

public async resolveCredentialOffer(credentialOffer: string): Promise<OpenId4VciResolvedCredentialOffer> {
public async resolveCredentialOffer(
agentContext: AgentContext,
credentialOffer: string
): Promise<OpenId4VciResolvedCredentialOffer> {
const client = await OpenID4VCIClient.fromURI({
uri: credentialOffer,
resolveOfferUri: true,
Expand All @@ -106,9 +101,7 @@ export class OpenId4VciHolderService {
}

const metadata = client.endpointMetadata
const credentialIssuerMetadata = metadata.credentialIssuerMetadata as
| CredentialIssuerMetadataV1_0_11
| CredentialIssuerMetadataV1_0_13
const credentialIssuerMetadata = metadata.credentialIssuerMetadata as OpenId4VciIssuerMetadata

if (!credentialIssuerMetadata) {
throw new CredoError(`Could not retrieve issuer metadata from '${metadata.issuer}'`)
Expand All @@ -128,18 +121,19 @@ export class OpenId4VciHolderService {
? credentialOfferPayload.credential_configuration_ids
: credentialOfferPayload.credentials

const offeredCredentials = getOfferedCredentials(
const { credentialConfigurationsSupported, credentialsSupported } = getOfferedCredentials(
agentContext,
offeredCredentialsData,
(credentialIssuerMetadata.credentials_supported as OpenId4VciCredentialSupportedWithId[] | undefined) ??
(credentialIssuerMetadata.credential_configurations_supported as OpenId4VciCredentialConfigurationsSupported)
credentialIssuerMetadata.credentials_supported ?? credentialIssuerMetadata.credential_configurations_supported
)

return {
metadata: {
...metadata,
credentialIssuerMetadata: credentialIssuerMetadata,
},
offeredCredentials,
offeredCredentials: credentialsSupported,
offeredCredentialConfigurations: credentialConfigurationsSupported,
credentialOfferPayload,
credentialOfferRequestWithBaseUrl: client.credentialOffer,
version: client.version(),
Expand Down Expand Up @@ -315,7 +309,7 @@ export class OpenId4VciHolderService {
}
) {
const { resolvedCredentialOffer, acceptCredentialOfferOptions } = options
const { metadata, version, offeredCredentials } = resolvedCredentialOffer
const { metadata, version, offeredCredentialConfigurations } = resolvedCredentialOffer

const { credentialsToRequest, credentialBindingResolver, verifyCredentialStatus } = acceptCredentialOfferOptions

Expand Down Expand Up @@ -357,24 +351,18 @@ export class OpenId4VciHolderService {
const receivedCredentials: Array<OpenId4VciCredentialResponse> = []
let newCNonce: string | undefined

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 CredoError(
`Credential to request '${credentialsToRequest[i]}' is not present in offered credentials. Offered credentials are ${offeredCredentialIds}`
)
}

return true
}) ?? offeredCredentials
const credentialConfigurationToRequest =
credentialsToRequest?.map((id) => {
if (!offeredCredentialConfigurations[id]) {
const offeredCredentialIds = Object.keys(offeredCredentialConfigurations).join(', ')
throw new CredoError(
`Credential to request '${id}' is not present in offered credentials. Offered credentials are ${offeredCredentialIds}`
)
}
return [id, offeredCredentialConfigurations[id]] as const
}) ?? Object.entries(offeredCredentialConfigurations)

const offeredCredentialConfigurations = credentialsSupportedV11ToV13(agentContext, credentialsSupportedToRequest)
for (const [offeredCredentialId, offeredCredentialConfiguration] of Object.entries(
offeredCredentialConfigurations
)) {
for (const [offeredCredentialId, offeredCredentialConfiguration] of credentialConfigurationToRequest) {
// 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,
Expand Down Expand Up @@ -570,10 +558,8 @@ export class OpenId4VciHolderService {
}
}

// FIXME credentialToRequest.credential_signing_alg_values_supported is only required for v11 compat
const proofSigningAlgsSupported =
credentialToRequest.configuration.proof_types_supported?.jwt?.proof_signing_alg_values_supported ??
credentialToRequest.configuration.credential_signing_alg_values_supported
credentialToRequest.configuration.proof_types_supported?.jwt?.proof_signing_alg_values_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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
OpenId4VciCredentialSupportedWithId,
OpenId4VciIssuerMetadata,
OpenId4VciCredentialOfferPayload,
OpenId4VciCredentialConfigurationsSupported,
} from '../shared'
import type { JwaSignatureAlgorithm, KeyType } from '@credo-ts/core'
import type { VerifiableCredential } from '@credo-ts/core/src/modules/dif-presentation-exchange/models/index'
Expand Down Expand Up @@ -56,6 +57,7 @@ export interface OpenId4VciResolvedCredentialOffer {
credentialOfferRequestWithBaseUrl: CredentialOfferRequestWithBaseUrl
credentialOfferPayload: OpenId4VciCredentialOfferPayload
offeredCredentials: OpenId4VciCredentialSupportedWithId[]
offeredCredentialConfigurations: OpenId4VciCredentialConfigurationsSupported
version: OpenId4VCIVersion
}

Expand Down
18 changes: 7 additions & 11 deletions packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import type {
OpenId4VciCredentialConfigurationsSupported,
OpenId4VciCredentialOfferPayload,
OpenId4VciCredentialRequest,
OpenId4VciCredentialSupportedWithId,
} from '../shared'
import type { AgentContext, DidDocument, Query, QueryOptions } from '@credo-ts/core'
import type {
Expand Down Expand Up @@ -52,10 +51,7 @@ import {
import { VcIssuerBuilder } from '@sphereon/oid4vci-issuer'

import { credentialsSupportedV11ToV13, OpenId4VciCredentialFormatProfile } from '../shared'
import {
credentialsSupportedV13ToV11,
getOfferedCredentialConfigurationsSupported,
} from '../shared/issuerMetadataUtils'
import { credentialsSupportedV13ToV11, getOfferedCredentials } from '../shared/issuerMetadataUtils'
import { storeActorIdForContextCorrelationId } from '../shared/router'
import { getSphereonVerifiableCredential } from '../shared/transform'
import { getProofTypeFromKey, isCredentialOfferV1Draft13 } from '../shared/utils'
Expand Down Expand Up @@ -121,7 +117,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
getOfferedCredentialConfigurationsSupported(
getOfferedCredentials(
agentContext,
options.offeredCredentials,
vcIssuer.issuerMetadata.credential_configurations_supported
Expand Down Expand Up @@ -435,21 +431,21 @@ export class OpenId4VcIssuerService {
agentContext: AgentContext,
credentialOffer: OpenId4VciCredentialOfferPayload,
credentialRequest: OpenId4VciCredentialRequest,
credentialsSupported: OpenId4VciCredentialSupportedWithId[] | OpenId4VciCredentialConfigurationsSupported,
allCredentialConfigurationsSupported: OpenId4VciCredentialConfigurationsSupported,
issuanceSession: OpenId4VcIssuanceSessionRecord
): OpenId4VciCredentialConfigurationsSupported {
const offeredCredentialsData = isCredentialOfferV1Draft13(credentialOffer)
? credentialOffer.credential_configuration_ids
: credentialOffer.credentials

const offeredCredentials = getOfferedCredentialConfigurationsSupported(
const { credentialConfigurationsSupported: offeredCredentialConfigurations } = getOfferedCredentials(
agentContext,
offeredCredentialsData,
credentialsSupported
allCredentialConfigurationsSupported
)

if ('credential_identifier' in credentialRequest && typeof credentialRequest.credential_identifier === 'string') {
const offeredCredential = offeredCredentials[credentialRequest.credential_identifier]
const offeredCredential = offeredCredentialConfigurations[credentialRequest.credential_identifier]
if (!offeredCredential) {
throw new CredoError(
`Requested credential with id '${credentialRequest.credential_identifier}' was not offered.`
Expand All @@ -462,7 +458,7 @@ export class OpenId4VcIssuerService {
}

return Object.fromEntries(
Object.entries(offeredCredentials).filter(([id, offeredCredential]) => {
Object.entries(offeredCredentialConfigurations).filter(([id, offeredCredential]) => {
if (offeredCredential.format !== credentialRequest.format) return false
if (issuanceSession.issuedCredentials.includes(id)) return false

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,9 @@ describe('OpenId4VcIssuer', () => {
userPinRequired: false,
},
})
).rejects.toThrow("Offered credential 'invalid id' is not part of credentials_supported of the issuer metadata.")
).rejects.toThrow(
"Offered credential 'invalid id' is not part of credentials_supported/credential_configurations_supported of the issuer metadata."
)
})

it('issuing non offered credential errors', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { Wallet } from '@credo-ts/core'

import { KeyType } from '@credo-ts/core'

import { getAgentContext } from '../../../../core/tests'
import { credentialsSupportedV11ToV13, credentialsSupportedV13ToV11 } from '../issuerMetadataUtils'

const agentContext = getAgentContext({
wallet: {
supportedKeyTypes: [KeyType.Ed25519, KeyType.P256],
} as Wallet,
})

describe('issuerMetadataUtils', () => {
describe('credentialsSupportedV13toV11', () => {
test('should correctly transform from v13 to v11 format', () => {
expect(
credentialsSupportedV13ToV11({
'pid-sd-jwt': {
scope: 'pid',
cryptographic_binding_methods_supported: ['jwk'],
credential_signing_alg_values_supported: ['ES256'],
proof_types_supported: {
jwt: {
proof_signing_alg_values_supported: ['ES256'],
},
},
vct: 'urn:eu.europa.ec.eudi:pid:1',
format: 'vc+sd-jwt',
},
})
).toEqual([
{
id: 'pid-sd-jwt',
scope: 'pid',
cryptographic_binding_methods_supported: ['jwk'],
cryptographic_suites_supported: ['ES256'],
vct: 'urn:eu.europa.ec.eudi:pid:1',
format: 'vc+sd-jwt',
},
])
})
})

describe('credentialsSupportedV11toV13', () => {
test('should correctly transform from v11 to v13 format', () => {
expect(
credentialsSupportedV11ToV13(agentContext, [
{
id: 'pid-sd-jwt',
scope: 'pid',
cryptographic_binding_methods_supported: ['jwk'],
cryptographic_suites_supported: ['ES256'],
vct: 'urn:eu.europa.ec.eudi:pid:1',
format: 'vc+sd-jwt',
},
])
).toEqual({
'pid-sd-jwt': {
scope: 'pid',
cryptographic_binding_methods_supported: ['jwk'],
credential_signing_alg_values_supported: ['ES256'],
proof_types_supported: {
jwt: {
proof_signing_alg_values_supported: ['ES256'],
},
},
vct: 'urn:eu.europa.ec.eudi:pid:1',
format: 'vc+sd-jwt',
order: undefined,
display: undefined,
claims: undefined,
},
})
})
})
})
Loading

0 comments on commit 35a04e3

Please sign in to comment.