Skip to content

Commit

Permalink
feat: dpop support (#1966)
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <[email protected]>
  • Loading branch information
auer-martin committed Aug 5, 2024
1 parent 35a04e3 commit fa62b74
Show file tree
Hide file tree
Showing 20 changed files with 371 additions and 138 deletions.
6 changes: 6 additions & 0 deletions .changeset/sharp-crews-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@credo-ts/core": patch
"@credo-ts/openid4vc": patch
---

Add support for Demonstrating Proof of Possesion (DPoP) when receiving credentials using OpenID4VCI
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"@sd-jwt/utils": "^0.7.0",
"@sphereon/pex": "^3.3.2",
"@sphereon/pex-models": "^2.2.4",
"@sphereon/ssi-types": "^0.23.0",
"@sphereon/ssi-types": "^0.28.0",
"@stablelib/ed25519": "^1.0.2",
"@types/ws": "^8.5.4",
"abort-controller": "^3.0.0",
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/modules/x509/X509ModuleConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export interface X509ModuleConfigOptions {
/**
*
* Array of trusted base64-encoded certificate strings in the DER-format.
*/
trustedCertificates?: [string, ...string[]]
}

Expand Down
11 changes: 6 additions & 5 deletions packages/openid4vc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@
},
"dependencies": {
"@credo-ts/core": "workspace:*",
"@sphereon/did-auth-siop": "0.15.1-next.4",
"@sphereon/oid4vci-client": "0.15.1-next.4",
"@sphereon/oid4vci-common": "0.15.1-next.4",
"@sphereon/oid4vci-issuer": "0.15.1-next.4",
"@sphereon/ssi-types": "0.26.1-next.132",
"@sphereon/did-auth-siop": "0.16.1-next.3",
"@sphereon/oid4vc-common": "0.16.1-next.3",
"@sphereon/oid4vci-client": "0.16.1-next.3",
"@sphereon/oid4vci-common": "0.16.1-next.3",
"@sphereon/oid4vci-issuer": "0.16.1-next.3",
"@sphereon/ssi-types": "0.28.0",
"class-transformer": "^0.5.1",
"rxjs": "^7.8.0"
},
Expand Down
14 changes: 8 additions & 6 deletions packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,12 @@ export class OpenId4VcHolderApi {
* @param options.code The authorization code obtained via the authorization request URI
*/
public async requestToken(options: OpenId4VciRequestTokenOptions): Promise<OpenId4VciRequestTokenResponse> {
const { access_token: accessToken, c_nonce: cNonce } = await this.openId4VciHolderService.requestAccessToken(
this.agentContext,
options
)
return { accessToken, cNonce }
const {
access_token: accessToken,
c_nonce: cNonce,
dpop,
} = await this.openId4VciHolderService.requestAccessToken(this.agentContext, options)
return { accessToken, cNonce, dpop }
}

/**
Expand All @@ -160,13 +161,14 @@ export class OpenId4VcHolderApi {
* @param options.tokenResponse Obtained through @see requestAccessToken
*/
public async requestCredentials(options: OpenId4VciRequestCredentialOptions) {
const { resolvedCredentialOffer, cNonce, accessToken, clientId, ...credentialRequestOptions } = options
const { resolvedCredentialOffer, cNonce, accessToken, dpop, clientId, ...credentialRequestOptions } = options

return this.openId4VciHolderService.acceptCredentialOffer(this.agentContext, {
resolvedCredentialOffer,
acceptCredentialOfferOptions: credentialRequestOptions,
accessToken,
cNonce,
dpop,
clientId,
})
}
Expand Down
131 changes: 108 additions & 23 deletions packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,81 @@
import type {
OpenId4VciAcceptCredentialOfferOptions,
OpenId4VciAuthCodeFlowOptions,
OpenId4VciProofOfPossessionRequirements,
OpenId4VciCredentialBindingResolver,
OpenId4VciResolvedCredentialOffer,
OpenId4VciCredentialResponse,
OpenId4VciNotificationEvent,
OpenId4VciProofOfPossessionRequirements,
OpenId4VciResolvedAuthorizationRequest,
OpenId4VciResolvedAuthorizationRequestWithCode,
OpenId4VciResolvedCredentialOffer,
OpenId4VciSupportedCredentialFormats,
OpenId4VciCredentialResponse,
OpenId4VciNotificationEvent,
OpenId4VciAcceptCredentialOfferOptions,
OpenId4VciTokenRequestOptions,
} from './OpenId4VciHolderServiceOptions'
import type {
OpenId4VciCredentialConfigurationSupported,
OpenId4VciCredentialSupported,
OpenId4VciIssuerMetadata,
} from '../shared'
import type { AgentContext, JwaSignatureAlgorithm, Key, JwkJson } from '@credo-ts/core'
import type { AgentContext, JwaSignatureAlgorithm, JwkJson, Key } from '@credo-ts/core'
import type {
AccessTokenResponse,
CredentialResponse,
Jwt,
OpenIDResponse,
AuthorizationDetails,
AuthorizationDetailsJwtVcJson,
AuthorizationDetailsJwtVcJsonLdAndLdpVc,
AuthorizationDetailsSdJwtVc,
CredentialResponse,
Jwt,
OpenIDResponse,
} from '@sphereon/oid4vci-common'

import {
SdJwtVcApi,
getJwkFromJson,
DidsApi,
CredoError,
DidsApi,
Hasher,
InjectionSymbols,
JsonEncoder,
Jwk,
JwsService,
Logger,
SdJwtVcApi,
SignatureSuiteRegistry,
TypedArrayEncoder,
W3cCredentialService,
W3cJsonLdVerifiableCredential,
W3cJwtVerifiableCredential,
getJwkClassFromJwaSignatureAlgorithm,
getJwkFromJson,
getJwkFromKey,
getKeyFromVerificationMethod,
getSupportedVerificationMethodTypesFromKeyType,
inject,
injectable,
parseDid,
} from '@credo-ts/core'
import { CreateDPoPClientOpts, CreateDPoPJwtPayloadProps, SigningAlgo } from '@sphereon/oid4vc-common'
import {
AccessTokenClient,
CredentialRequestClientBuilder,
ProofOfPossessionBuilder,
OpenID4VCIClient,
OpenID4VCIClientStateV1_0_13,
OpenID4VCIClientV1_0_11,
OpenID4VCIClientV1_0_13,
OpenID4VCIClientStateV1_0_13,
ProofOfPossessionBuilder,
} from '@sphereon/oid4vci-client'
import { CodeChallengeMethod, OpenId4VCIVersion, PARMode, post } from '@sphereon/oid4vci-common'
import {
CodeChallengeMethod,
DPoPResponseParams,
EndpointMetadataResult,
OpenId4VCIVersion,
PARMode,
post,
} from '@sphereon/oid4vci-common'

import { OpenId4VciCredentialFormatProfile } from '../shared'
import { getTypesFromCredentialSupported, getOfferedCredentials } from '../shared/issuerMetadataUtils'
import { getSupportedJwaSignatureAlgorithms, isCredentialOfferV1Draft13 } from '../shared/utils'
import { getOfferedCredentials, getTypesFromCredentialSupported } from '../shared/issuerMetadataUtils'
import { getCreateJwtCallback, getSupportedJwaSignatureAlgorithms, isCredentialOfferV1Draft13 } from '../shared/utils'

import { openId4VciSupportedCredentialFormats, OpenId4VciNotificationMetadata } from './OpenId4VciHolderServiceOptions'
import { OpenId4VciNotificationMetadata, openId4VciSupportedCredentialFormats } from './OpenId4VciHolderServiceOptions'

@injectable()
export class OpenId4VciHolderService {
Expand Down Expand Up @@ -260,14 +269,69 @@ export class OpenId4VciHolderService {
}
}

private async getCreateDpopOptions(
agentContext: AgentContext,
metadata: Pick<EndpointMetadataResult, 'authorizationServerMetadata'> & {
credentialIssuerMetadata: OpenId4VciIssuerMetadata
},
resourceRequestOptions?: {
jwk: Jwk
jwtPayloadProps: Omit<CreateDPoPJwtPayloadProps, 'htu' | 'htm'>
}
) {
const dpopSigningAlgValuesSupported =
metadata.authorizationServerMetadata?.dpop_signing_alg_values_supported ??
metadata.credentialIssuerMetadata.dpop_signing_alg_values_supported

if (!dpopSigningAlgValuesSupported) return undefined

const alg = dpopSigningAlgValuesSupported.find((alg) => getJwkClassFromJwaSignatureAlgorithm(alg))

let jwk: Jwk
if (resourceRequestOptions) {
jwk = resourceRequestOptions.jwk
} else {
const JwkClass = alg ? getJwkClassFromJwaSignatureAlgorithm(alg) : undefined

if (!JwkClass) {
throw new CredoError(
`No supported dpop signature algorithms found in dpop_signing_alg_values_supported '${dpopSigningAlgValuesSupported.join(
', '
)}'`
)
}

const key = await agentContext.wallet.createKey({ keyType: JwkClass.keyType })
jwk = getJwkFromKey(key)
}

const createDPoPOpts: CreateDPoPClientOpts = {
jwtIssuer: { alg: alg as unknown as SigningAlgo, jwk: jwk.toJson() },
dPoPSigningAlgValuesSupported: dpopSigningAlgValuesSupported,
jwtPayloadProps: resourceRequestOptions?.jwtPayloadProps ?? {},
createJwtCallback: getCreateJwtCallback(agentContext),
}
return createDPoPOpts
}

public async requestAccessToken(agentContext: AgentContext, options: OpenId4VciTokenRequestOptions) {
const { resolvedCredentialOffer, txCode, resolvedAuthorizationRequest, code } = options
const { metadata, credentialOfferRequestWithBaseUrl } = resolvedCredentialOffer

// acquire the access token
let accessTokenResponse: OpenIDResponse<AccessTokenResponse>
let accessTokenResponse: OpenIDResponse<AccessTokenResponse, DPoPResponseParams>

const accessTokenClient = new AccessTokenClient()

const createDPoPOpts = await this.getCreateDpopOptions(agentContext, metadata)

let dpopJwk: Jwk | undefined
if (createDPoPOpts) {
if (!createDPoPOpts.jwtIssuer.jwk.kty) {
throw new CredoError('Missing required key type (kty) in the jwk.')
}
dpopJwk = getJwkFromJson(createDPoPOpts.jwtIssuer.jwk as JwkJson)
}
if (resolvedAuthorizationRequest) {
const { codeVerifier, redirectUri } = resolvedAuthorizationRequest
accessTokenResponse = await accessTokenClient.acquireAccessToken({
Expand All @@ -277,12 +341,14 @@ export class OpenId4VciHolderService {
code,
codeVerifier,
redirectUri,
createDPoPOpts,
})
} else {
accessTokenResponse = await accessTokenClient.acquireAccessToken({
metadata: metadata,
credentialOffer: { credential_offer: credentialOfferRequestWithBaseUrl.credential_offer },
pin: txCode,
createDPoPOpts,
})
}

Expand All @@ -294,7 +360,10 @@ export class OpenId4VciHolderService {

this.logger.debug('Requested OpenId4VCI Access Token.')

return accessTokenResponse.successBody
return {
...accessTokenResponse.successBody,
...(dpopJwk && { dpop: { jwk: dpopJwk, nonce: accessTokenResponse.params?.dpop?.dpopNonce } }),
}
}

public async acceptCredentialOffer(
Expand All @@ -305,6 +374,7 @@ export class OpenId4VciHolderService {
resolvedAuthorizationRequestWithCode?: OpenId4VciResolvedAuthorizationRequestWithCode
accessToken?: string
cNonce?: string
dpop?: { jwk: Jwk; nonce?: string }
clientId?: string
}
) {
Expand All @@ -318,7 +388,9 @@ export class OpenId4VciHolderService {
return []
}

this.logger.info(`Accepting the following credential offers '${credentialsToRequest}'`)
this.logger.info(
`Accepting the following credential offers '${credentialsToRequest ? credentialsToRequest.join(', ') : 'all'}`
)

const supportedJwaSignatureAlgorithms = getSupportedJwaSignatureAlgorithms(agentContext)

Expand All @@ -345,7 +417,11 @@ export class OpenId4VciHolderService {
} as OpenId4VciTokenRequestOptions

const tokenResponse = options.accessToken
? { access_token: options.accessToken, c_nonce: options.cNonce }
? {
access_token: options.accessToken,
c_nonce: options.cNonce,
dpop: options.dpop,
}
: await this.requestAccessToken(agentContext, tokenRequestOptions)

const receivedCredentials: Array<OpenId4VciCredentialResponse> = []
Expand Down Expand Up @@ -413,10 +489,19 @@ export class OpenId4VciHolderService {
.withToken(tokenResponse.access_token)

const credentialRequestClient = credentialRequestBuilder.build()

const createDpopOpts = tokenResponse.dpop
? await this.getCreateDpopOptions(agentContext, metadata, {
jwk: tokenResponse.dpop.jwk,
jwtPayloadProps: { accessToken: tokenResponse.access_token, nonce: tokenResponse.dpop?.nonce },
})
: undefined

const credentialResponse = await credentialRequestClient.acquireCredentialsUsingProof({
proofInput: proofOfPossession,
credentialTypes: getTypesFromCredentialSupported(offeredCredentialConfiguration),
format: offeredCredentialConfiguration.format,
createDPoPOpts: createDpopOpts,
})

newCNonce = credentialResponse.successBody?.c_nonce
Expand Down Expand Up @@ -611,7 +696,7 @@ export class OpenId4VciHolderService {

private async handleCredentialResponse(
agentContext: AgentContext,
credentialResponse: OpenIDResponse<CredentialResponse>,
credentialResponse: OpenIDResponse<CredentialResponse, DPoPResponseParams>,
options: {
verifyCredentialStatus: boolean
credentialIssuerMetadata: OpenId4VciIssuerMetadata
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {
OpenId4VciCredentialOfferPayload,
OpenId4VciCredentialConfigurationsSupported,
} from '../shared'
import type { JwaSignatureAlgorithm, KeyType } from '@credo-ts/core'
import type { JwaSignatureAlgorithm, Jwk, KeyType } from '@credo-ts/core'
import type { VerifiableCredential } from '@credo-ts/core/src/modules/dif-presentation-exchange/models/index'
import type {
AccessTokenResponse,
Expand Down Expand Up @@ -43,7 +43,11 @@ export type OpenId4VciNotificationEvent = 'credential_accepted' | 'credential_fa

export type OpenId4VciTokenResponse = Pick<AccessTokenResponse, 'access_token' | 'c_nonce'>

export type OpenId4VciRequestTokenResponse = { accessToken: string; cNonce?: string }
export type OpenId4VciRequestTokenResponse = {
accessToken: string
cNonce?: string
dpop?: { jwk: Jwk; nonce?: string }
}

export interface OpenId4VciCredentialResponse {
credential: VerifiableCredential
Expand Down Expand Up @@ -114,6 +118,7 @@ export interface OpenId4VciCredentialRequestOptions extends Omit<OpenId4VciAccep
resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer
accessToken: string
cNonce?: string
dpop?: { jwk: Jwk; nonce?: string }

/**
* The client id used for authorization. Only required if authorization_code flow was used.
Expand Down
Loading

0 comments on commit fa62b74

Please sign in to comment.