diff --git a/.changeset/sharp-crews-sniff.md b/.changeset/sharp-crews-sniff.md new file mode 100644 index 0000000000..c6d48c6494 --- /dev/null +++ b/.changeset/sharp-crews-sniff.md @@ -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 diff --git a/packages/core/package.json b/packages/core/package.json index 78fc955ae3..5904500d8c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -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", diff --git a/packages/core/src/modules/x509/X509ModuleConfig.ts b/packages/core/src/modules/x509/X509ModuleConfig.ts index ac44bb02bc..5fcd99a076 100644 --- a/packages/core/src/modules/x509/X509ModuleConfig.ts +++ b/packages/core/src/modules/x509/X509ModuleConfig.ts @@ -1,4 +1,8 @@ export interface X509ModuleConfigOptions { + /** + * + * Array of trusted base64-encoded certificate strings in the DER-format. + */ trustedCertificates?: [string, ...string[]] } diff --git a/packages/openid4vc/package.json b/packages/openid4vc/package.json index 3995843668..fc72ee6d16 100644 --- a/packages/openid4vc/package.json +++ b/packages/openid4vc/package.json @@ -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" }, diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts index 4e68a79c8d..1a9dd4ecd8 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts @@ -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 { - 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 } } /** @@ -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, }) } diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts index 598cb6c849..ef0f3041c6 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts @@ -1,14 +1,14 @@ import type { + OpenId4VciAcceptCredentialOfferOptions, OpenId4VciAuthCodeFlowOptions, - OpenId4VciProofOfPossessionRequirements, OpenId4VciCredentialBindingResolver, - OpenId4VciResolvedCredentialOffer, + OpenId4VciCredentialResponse, + OpenId4VciNotificationEvent, + OpenId4VciProofOfPossessionRequirements, OpenId4VciResolvedAuthorizationRequest, OpenId4VciResolvedAuthorizationRequestWithCode, + OpenId4VciResolvedCredentialOffer, OpenId4VciSupportedCredentialFormats, - OpenId4VciCredentialResponse, - OpenId4VciNotificationEvent, - OpenId4VciAcceptCredentialOfferOptions, OpenId4VciTokenRequestOptions, } from './OpenId4VciHolderServiceOptions' import type { @@ -16,34 +16,35 @@ import type { 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, @@ -51,22 +52,30 @@ import { 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 { @@ -260,14 +269,69 @@ export class OpenId4VciHolderService { } } + private async getCreateDpopOptions( + agentContext: AgentContext, + metadata: Pick & { + credentialIssuerMetadata: OpenId4VciIssuerMetadata + }, + resourceRequestOptions?: { + jwk: Jwk + jwtPayloadProps: Omit + } + ) { + 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 + let accessTokenResponse: OpenIDResponse 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({ @@ -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, }) } @@ -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( @@ -305,6 +374,7 @@ export class OpenId4VciHolderService { resolvedAuthorizationRequestWithCode?: OpenId4VciResolvedAuthorizationRequestWithCode accessToken?: string cNonce?: string + dpop?: { jwk: Jwk; nonce?: string } clientId?: string } ) { @@ -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) @@ -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 = [] @@ -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 @@ -611,7 +696,7 @@ export class OpenId4VciHolderService { private async handleCredentialResponse( agentContext: AgentContext, - credentialResponse: OpenIDResponse, + credentialResponse: OpenIDResponse, options: { verifyCredentialStatus: boolean credentialIssuerMetadata: OpenId4VciIssuerMetadata diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderServiceOptions.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderServiceOptions.ts index 6673f01bf0..0bd7ad0e8d 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderServiceOptions.ts @@ -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, @@ -43,7 +43,11 @@ export type OpenId4VciNotificationEvent = 'credential_accepted' | 'credential_fa export type OpenId4VciTokenResponse = Pick -export type OpenId4VciRequestTokenResponse = { accessToken: string; cNonce?: string } +export type OpenId4VciRequestTokenResponse = { + accessToken: string + cNonce?: string + dpop?: { jwk: Jwk; nonce?: string } +} export interface OpenId4VciCredentialResponse { credential: VerifiableCredential @@ -114,6 +118,7 @@ export interface OpenId4VciCredentialRequestOptions extends Omit & - (OpenId4VcIssuerRecordCredentialSupportedProps | OpenId4VcIssuerRecordCredentialConfigurationsSupportedProps) - ) { - const issuer = await this.openId4VcIssuerService.getIssuerByIssuerId(this.agentContext, options.issuerId) + public async updateIssuerMetadata(options: OpenId4VcUpdateIssuerRecordOptions) { + const { issuerId, credentialConfigurationsSupported, credentialsSupported, ...issuerOptions } = options + + const issuer = await this.openId4VcIssuerService.getIssuerByIssuerId(this.agentContext, issuerId) - if (options.credentialConfigurationsSupported) { - issuer.credentialConfigurationsSupported = options.credentialConfigurationsSupported - issuer.credentialsSupported = credentialsSupportedV13ToV11(options.credentialConfigurationsSupported) + if (credentialConfigurationsSupported) { + issuer.credentialConfigurationsSupported = credentialConfigurationsSupported + issuer.credentialsSupported = credentialsSupportedV13ToV11(credentialConfigurationsSupported) } else { - issuer.credentialsSupported = options.credentialsSupported + issuer.credentialsSupported = credentialsSupported issuer.credentialConfigurationsSupported = undefined } - issuer.display = options.display + issuer.display = issuerOptions.display + issuer.dpopSigningAlgValuesSupported = issuerOptions.dpopSigningAlgValuesSupported return this.openId4VcIssuerService.updateIssuer(this.agentContext, issuer) } diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts index df772925ac..b26b35952a 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts @@ -299,6 +299,7 @@ export class OpenId4VcIssuerService { const openId4VcIssuerBase = { issuerId: options.issuerId ?? utils.uuid(), display: options.display, + dpopSigningAlgValuesSupported: options.dpopSigningAlgValuesSupported, accessTokenPublicKeyFingerprint: accessTokenSignerKey.fingerprint, } as const @@ -340,6 +341,7 @@ export class OpenId4VcIssuerService { issuerRecord.credentialConfigurationsSupported ?? credentialsSupportedV11ToV13(agentContext, issuerRecord.credentialsSupported), issuerDisplay: issuerRecord.display, + dpopSigningAlgValuesSupported: issuerRecord.dpopSigningAlgValuesSupported, } satisfies OpenId4VcIssuerMetadata return issuerMetadata diff --git a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts index 8914af5b5d..58c492abe8 100644 --- a/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts +++ b/packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerServiceOptions.ts @@ -2,6 +2,7 @@ import type { OpenId4VcIssuanceSessionRecord, OpenId4VcIssuerRecordCredentialConfigurationsSupportedProps, OpenId4VcIssuerRecordCredentialSupportedProps, + OpenId4VcIssuerRecordProps, } from './repository' import type { OpenId4VcCredentialHolderBinding, @@ -13,7 +14,13 @@ import type { OpenId4VciIssuerMetadataDisplay, OpenId4VciTxCode, } from '../shared' -import type { AgentContext, ClaimFormat, W3cCredential, SdJwtVcSignOptions } from '@credo-ts/core' +import type { + AgentContext, + ClaimFormat, + W3cCredential, + SdJwtVcSignOptions, + JwaSignatureAlgorithm, +} from '@credo-ts/core' export interface OpenId4VciPreAuthorizedCodeFlowConfig { preAuthorizedCode?: string @@ -39,6 +46,7 @@ export type OpenId4VcIssuerMetadata = { issuerDisplay?: OpenId4VciIssuerMetadataDisplay[] credentialsSupported: OpenId4VciCredentialSupportedWithId[] credentialConfigurationsSupported: OpenId4VciCredentialConfigurationsSupported + dpopSigningAlgValuesSupported?: [JwaSignatureAlgorithm, ...JwaSignatureAlgorithm[]] } export interface OpenId4VciCreateCredentialOfferOptions { @@ -152,4 +160,11 @@ export type OpenId4VciCreateIssuerOptions = { issuerId?: string display?: OpenId4VciIssuerMetadataDisplay[] + dpopSigningAlgValuesSupported?: [JwaSignatureAlgorithm, ...JwaSignatureAlgorithm[]] } & (OpenId4VcIssuerRecordCredentialSupportedProps | OpenId4VcIssuerRecordCredentialConfigurationsSupportedProps) + +export type OpenId4VcUpdateIssuerRecordOptions = Pick< + OpenId4VcIssuerRecordProps, + 'issuerId' | 'display' | 'dpopSigningAlgValuesSupported' +> & + (OpenId4VcIssuerRecordCredentialSupportedProps | OpenId4VcIssuerRecordCredentialConfigurationsSupportedProps) diff --git a/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRecord.ts b/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRecord.ts index b653265cde..91e390bed3 100644 --- a/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRecord.ts +++ b/packages/openid4vc/src/openid4vc-issuer/repository/OpenId4VcIssuerRecord.ts @@ -3,7 +3,7 @@ import type { OpenId4VciCredentialConfigurationsSupported, OpenId4VciIssuerMetadataDisplay, } from '../../shared' -import type { RecordTags, TagsBase } from '@credo-ts/core' +import type { JwaSignatureAlgorithm, RecordTags, TagsBase } from '@credo-ts/core' import { BaseRecord, utils } from '@credo-ts/core' @@ -38,6 +38,12 @@ export type OpenId4VcIssuerRecordProps = { */ accessTokenPublicKeyFingerprint: string + /** + * The DPoP signing algorithms supported by this issuer. + * If not provided, dPoP is considered unsupported. + */ + dpopSigningAlgValuesSupported?: [JwaSignatureAlgorithm, ...JwaSignatureAlgorithm[]] + display?: OpenId4VciIssuerMetadataDisplay[] } & (OpenId4VcIssuerRecordCredentialSupportedProps | OpenId4VcIssuerRecordCredentialConfigurationsSupportedProps) @@ -56,6 +62,7 @@ export class OpenId4VcIssuerRecord extends BaseRecord export type OpenId4VciTxCode = TxCode -// FIXME: https://github.com/Sphereon-Opensource/OID4VC/pull/136 -export type OpenId4VciIssuerMetadataV1Draft11 = Omit & { - credentials_supported: OpenId4VciCredentialSupported[] -} +export type OpenId4VciIssuerMetadataV1Draft11 = CredentialIssuerMetadataV1_0_11 export type OpenId4VciIssuerMetadataV1Draft13 = CredentialIssuerMetadataV1_0_13 - export type OpenId4VciIssuerMetadata = OpenId4VciIssuerMetadataV1Draft11 | OpenId4VciIssuerMetadataV1Draft13 + export type OpenId4VciIssuerMetadataDisplay = MetadataDisplay export type OpenId4VciCredentialRequest = diff --git a/packages/openid4vc/src/shared/utils.ts b/packages/openid4vc/src/shared/utils.ts index cd77b3b82a..5c50029da6 100644 --- a/packages/openid4vc/src/shared/utils.ts +++ b/packages/openid4vc/src/shared/utils.ts @@ -1,6 +1,7 @@ import type { OpenId4VcJwtIssuer } from './models' -import type { AgentContext, JwaSignatureAlgorithm, JwsProtectedHeaderOptions, Key } from '@credo-ts/core' -import type { CreateJwtCallback, JwtIssuer, SigningAlgo, VerifyJwtCallback } from '@sphereon/did-auth-siop' +import type { AgentContext, JwaSignatureAlgorithm, JwkJson, Key } from '@credo-ts/core' +import type { JwtIssuerWithContext as VpJwtIssuerWithContext, VerifyJwtCallback } from '@sphereon/did-auth-siop' +import type { DPoPJwtIssuerWithContext, CreateJwtCallback, JwtIssuer } from '@sphereon/oid4vc-common' import type { CredentialOfferPayloadV1_0_11, CredentialOfferPayloadV1_0_13 } from '@sphereon/oid4vci-common' import { @@ -66,23 +67,29 @@ export function getVerifyJwtCallback(agentContext: AgentContext): VerifyJwtCallb } } -export function getCreateJwtCallback(agentContext: AgentContext): CreateJwtCallback { +export function getCreateJwtCallback( + agentContext: AgentContext +): CreateJwtCallback { return async (jwtIssuer, jwt) => { const jwsService = agentContext.dependencyManager.resolve(JwsService) if (jwtIssuer.method === 'did') { const key = await getKeyFromDid(agentContext, jwtIssuer.didUrl) const jws = await jwsService.createJwsCompact(agentContext, { - protectedHeaderOptions: { alg: jwtIssuer.alg, ...jwt.header }, + protectedHeaderOptions: { ...jwt.header, alg: jwtIssuer.alg, jwk: undefined }, payload: JwtPayload.fromJson(jwt.payload), key, }) return jws } else if (jwtIssuer.method === 'jwk') { - const key = getJwkFromJson(jwtIssuer.jwk).key + if (!jwtIssuer.jwk.kty) { + throw new CredoError('Missing required key type (kty) in the jwk.') + } + const jwk = getJwkFromJson(jwtIssuer.jwk as JwkJson) + const key = jwk.key const jws = await jwsService.createJwsCompact(agentContext, { - protectedHeaderOptions: jwt.header as JwsProtectedHeaderOptions, + protectedHeaderOptions: { ...jwt.header, jwk, alg: jwtIssuer.alg }, payload: JwtPayload.fromJson(jwt.payload), key, }) @@ -92,7 +99,7 @@ export function getCreateJwtCallback(agentContext: AgentContext): CreateJwtCallb const key = X509Service.getLeafCertificate(agentContext, { certificateChain: jwtIssuer.x5c }).publicKey const jws = await jwsService.createJwsCompact(agentContext, { - protectedHeaderOptions: jwt.header as JwsProtectedHeaderOptions, + protectedHeaderOptions: { ...jwt.header, alg: jwtIssuer.alg, jwk: undefined }, payload: JwtPayload.fromJson(jwt.payload), key, }) @@ -110,13 +117,13 @@ export async function openIdTokenIssuerToJwtIssuer( ): Promise { if (openId4VcTokenIssuer.method === 'did') { const key = await getKeyFromDid(agentContext, openId4VcTokenIssuer.didUrl) - const _alg = getJwkClassFromKeyType(key.keyType)?.supportedSignatureAlgorithms[0] - if (!_alg) throw new CredoError(`No supported signature algorithms for key type: ${key.keyType}`) + const alg = getJwkClassFromKeyType(key.keyType)?.supportedSignatureAlgorithms[0] + if (!alg) throw new CredoError(`No supported signature algorithms for key type: ${key.keyType}`) return { method: openId4VcTokenIssuer.method, didUrl: openId4VcTokenIssuer.didUrl, - alg: _alg as unknown as SigningAlgo, + alg, } } else if (openId4VcTokenIssuer.method === 'x5c') { const issuer = openId4VcTokenIssuer.issuer @@ -124,6 +131,12 @@ export async function openIdTokenIssuerToJwtIssuer( certificateChain: openId4VcTokenIssuer.x5c, }) + const jwk = getJwkFromKey(leafCertificate.publicKey) + const alg = jwk.supportedSignatureAlgorithms[0] + if (!alg) { + throw new CredoError(`No supported signature algorithms found key type: '${jwk.keyType}'`) + } + if (!issuer.startsWith('https://')) { throw new CredoError('The X509 certificate issuer must be a HTTPS URI.') } @@ -131,6 +144,7 @@ export async function openIdTokenIssuerToJwtIssuer( if (leafCertificate.sanUriNames?.includes(issuer)) { return { ...openId4VcTokenIssuer, + alg, clientIdScheme: 'x509_san_uri', } } else { @@ -142,12 +156,23 @@ export async function openIdTokenIssuerToJwtIssuer( return { ...openId4VcTokenIssuer, + alg, clientIdScheme: 'x509_san_dns', } } + } else if (openId4VcTokenIssuer.method === 'jwk') { + const alg = openId4VcTokenIssuer.jwk.supportedSignatureAlgorithms[0] + if (!alg) { + throw new CredoError(`No supported signature algorithms for key type: '${openId4VcTokenIssuer.jwk.keyType}'`) + } + return { + ...openId4VcTokenIssuer, + jwk: openId4VcTokenIssuer.jwk.toJson(), + alg, + } } - return openId4VcTokenIssuer + throw new CredoError(`Unsupported jwt issuer method '${(openId4VcTokenIssuer as OpenId4VcJwtIssuer).method}'`) } export function getProofTypeFromKey(agentContext: AgentContext, key: Key) { diff --git a/packages/openid4vc/tests/openid4vc.e2e.test.ts b/packages/openid4vc/tests/openid4vc.e2e.test.ts index 584c4b5947..6cad2cfeab 100644 --- a/packages/openid4vc/tests/openid4vc.e2e.test.ts +++ b/packages/openid4vc/tests/openid4vc.e2e.test.ts @@ -16,6 +16,8 @@ import { W3cCredentialSubject, w3cDate, W3cIssuer, + Jwt, + Jwk, } from '@credo-ts/core' import express, { type Express } from 'express' @@ -190,11 +192,14 @@ describe('OpenId4Vc', () => { const issuerTenant2 = await issuer.agent.modules.tenants.getTenantAgent({ tenantId: issuer2.tenantId }) const openIdIssuerTenant1 = await issuerTenant1.modules.openId4VcIssuer.createIssuer({ + dpopSigningAlgValuesSupported: [JwaSignatureAlgorithm.EdDSA], credentialConfigurationsSupported: { universityDegree: universityDegreeCredentialConfigurationSupported, }, }) const issuer1Record = await issuerTenant1.modules.openId4VcIssuer.getIssuerByIssuerId(openIdIssuerTenant1.issuerId) + expect(issuer1Record.dpopSigningAlgValuesSupported).toEqual(['EdDSA']) + expect(issuer1Record.credentialsSupported).toEqual([ { id: 'universityDegree', @@ -218,6 +223,7 @@ describe('OpenId4Vc', () => { }, }) const openIdIssuerTenant2 = await issuerTenant2.modules.openId4VcIssuer.createIssuer({ + dpopSigningAlgValuesSupported: [JwaSignatureAlgorithm.EdDSA], credentialsSupported: [universityDegreeCredentialSdJwt2], }) @@ -257,6 +263,9 @@ describe('OpenId4Vc', () => { credentialOffer1 ) + expect(resolvedCredentialOffer1.metadata.credentialIssuerMetadata?.dpop_signing_alg_values_supported).toEqual([ + 'EdDSA', + ]) expect(resolvedCredentialOffer1.offeredCredentials).toEqual([ { id: 'universityDegree', @@ -278,10 +287,20 @@ describe('OpenId4Vc', () => { ) // Bind to JWK - const credentialsTenant1 = await holderTenant1.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode( - resolvedCredentialOffer1, - { credentialBindingResolver, userPin: issuanceSession1.userPin } - ) + const tokenResponseTenant1 = await holderTenant1.modules.openId4VcHolder.requestToken({ + resolvedCredentialOffer: resolvedCredentialOffer1, + }) + + expect(tokenResponseTenant1.accessToken).toBeDefined() + expect(tokenResponseTenant1.dpop?.jwk).toBeInstanceOf(Jwk) + const { payload } = Jwt.fromSerializedJwt(tokenResponseTenant1.accessToken) + expect(payload.additionalClaims.token_type).toEqual('DPoP') + + const credentialsTenant1 = await holderTenant1.modules.openId4VcHolder.requestCredentials({ + resolvedCredentialOffer: resolvedCredentialOffer1, + ...tokenResponseTenant1, + credentialBindingResolver, + }) // Wait for all events await waitForCredentialIssuanceSessionRecordSubject(issuer.replaySubject, { @@ -306,7 +325,7 @@ describe('OpenId4Vc', () => { }) expect(credentialsTenant1).toHaveLength(1) - const compactSdJwtVcTenant1 = (credentialsTenant1[0] as SdJwtVc).compact + const compactSdJwtVcTenant1 = (credentialsTenant1[0].credential as SdJwtVc).compact const sdJwtVcTenant1 = holderTenant1.sdJwtVc.fromCompact(compactSdJwtVcTenant1) expect(sdJwtVcTenant1.payload.vct).toEqual('UniversityDegreeCredential') diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1f5fe8ac39..b7a575c934 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -454,8 +454,8 @@ importers: specifier: ^2.2.4 version: 2.2.4 '@sphereon/ssi-types': - specifier: ^0.23.0 - version: 0.23.4 + specifier: ^0.28.0 + version: 0.28.0 '@stablelib/ed25519': specifier: ^1.0.2 version: 1.0.3 @@ -684,20 +684,23 @@ importers: specifier: workspace:* version: link:../core '@sphereon/did-auth-siop': - specifier: 0.15.1-next.4 - version: 0.15.1-next.4 + specifier: 0.16.1-next.3 + version: 0.16.1-next.3 + '@sphereon/oid4vc-common': + specifier: 0.16.1-next.3 + version: 0.16.1-next.3 '@sphereon/oid4vci-client': - specifier: 0.15.1-next.4 - version: 0.15.1-next.4 + specifier: 0.16.1-next.3 + version: 0.16.1-next.3 '@sphereon/oid4vci-common': - specifier: 0.15.1-next.4 - version: 0.15.1-next.4 + specifier: 0.16.1-next.3 + version: 0.16.1-next.3 '@sphereon/oid4vci-issuer': - specifier: 0.15.1-next.4 - version: 0.15.1-next.4 + specifier: 0.16.1-next.3 + version: 0.16.1-next.3 '@sphereon/ssi-types': - specifier: 0.26.1-next.132 - version: 0.26.1-next.132 + specifier: 0.28.0 + version: 0.28.0 class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -2380,23 +2383,27 @@ packages: resolution: {integrity: sha512-kQpk267uxB19X3X2T1mvNMjyvIEonpNSHrMlK5ZaBU6aZxw7wPbpgKJOjHN3+/GPVpXgAV9soVT2oyHpLkLtyw==} engines: {node: '>= 8'} - '@sphereon/did-auth-siop@0.15.1-next.4': - resolution: {integrity: sha512-Rg5O6A0P6uqf/a5o5lrCxt2WGlpzemyHCrkiOjvYAWGxGCxGctpKRVCNgwxDdFpIFCuXICNi0l/lk4iuZ1ofmQ==} + '@sphereon/did-auth-siop@0.16.1-next.3': + resolution: {integrity: sha512-PjE1n5oUPpLU7KCZ3pK1OOo+/DL+ASsJew5I2awt0HT7+mOEbXPhIWmnKjIcqPnXrw8O59m8uYi5MUrtASrIpg==} engines: {node: '>=18'} '@sphereon/did-uni-client@0.6.3': resolution: {integrity: sha512-g7LD7ofbE36slHN7Bhr5dwUrj6t0BuZeXBYJMaVY/pOeL1vJxW1cZHbZqu0NSfOmzyBg4nsYVlgTjyi/Aua2ew==} - '@sphereon/oid4vci-client@0.15.1-next.4': - resolution: {integrity: sha512-HfSD5aa1dTMlx3D5QpEkYwHxbbSnvb8eEmv4xno7b9/WahNp4xmQeP9jxUz9K0VjIYNvAtCROEEbrgEo9F6iQA==} + '@sphereon/oid4vc-common@0.16.1-next.3': + resolution: {integrity: sha512-0bdCLsUtqzmn/Zks0RZNuO8H3s5Zc71x/o1WDXE4263REvO8zuzI4N6viNQZdM8vUxR99GnADQCxWCY2ZoVO8g==} engines: {node: '>=18'} - '@sphereon/oid4vci-common@0.15.1-next.4': - resolution: {integrity: sha512-X5NKqfc59+D+sql3QU91WJrbWHYDL8nfZCUT0X8UCTIcc5ioUlw1rWxv8CG2HUIuNXA2Uf/Nvm/9UZkinELxQQ==} + '@sphereon/oid4vci-client@0.16.1-next.3': + resolution: {integrity: sha512-b5xtDlQcEsg0W0In1lywUcvf3jJy/NhfvknbtYjRVtObhA2K/pRsb1yS+p54Vfc02enA+A/a2lVb6zI2peyF3w==} engines: {node: '>=18'} - '@sphereon/oid4vci-issuer@0.15.1-next.4': - resolution: {integrity: sha512-2C4KlhQY55PcmBHI4CVVHMdP3QntmEUgxoheHw4kRLc5ntsmeLrUkO8ZcwHZybXb981xjboN37v8k01j2jSLcg==} + '@sphereon/oid4vci-common@0.16.1-next.3': + resolution: {integrity: sha512-+0Cm/qWgQ2efs+vSFwf37Zji1k+oSs3pI7yuN0dGQF7iYG3whq+iNRfIKR5M9rYg8H4bITg0PSTrcFXz7VuWww==} + engines: {node: '>=18'} + + '@sphereon/oid4vci-issuer@0.16.1-next.3': + resolution: {integrity: sha512-92D+dEqvEkyI6XKkjbZXSmZlqLNdl3vAd8BNDWM1+E2X337lFnT9cE0pF432YdOzvS/ZwdlmftGjq2ehi1nOgg==} engines: {node: '>=18'} peerDependencies: awesome-qr: ^2.1.5-rc.0 @@ -2414,11 +2421,8 @@ packages: '@sphereon/ssi-types@0.22.0': resolution: {integrity: sha512-YPJAZlKmzNALXK8ohP3ETxj1oVzL4+M9ljj3fD5xrbacvYax1JPCVKc8BWSubGcQckKHPbgbpcS7LYEeghyT9Q==} - '@sphereon/ssi-types@0.23.4': - resolution: {integrity: sha512-1lM2yfOEhpcYYBxm/12KYY4n3ZSahVf5rFqGdterQkMJMthwr20HqTjw3+VK5p7IVf+86DyBoZJyS4V9tSsoCA==} - - '@sphereon/ssi-types@0.26.1-next.132': - resolution: {integrity: sha512-coqhozmVf2phy40l1htj/zbLJLJhyD69ZukBVR4gKn35t8q2B2EZhJbCIPWKk2HWT6+gV+JUBQ9n4v3cSHNCJg==} + '@sphereon/ssi-types@0.28.0': + resolution: {integrity: sha512-NkTkrsBoQUZzJutlk5XD3snBxL9kfsxKdQvBbGUEaUDOiW8siTNUoJuQFeA+bI0eJY99up95bmMKdJeDc1VDfg==} '@sphereon/ssi-types@0.9.0': resolution: {integrity: sha512-umCr/syNcmvMMbQ+i/r/mwjI1Qw2aFPp9AwBTvTo1ailAVaaJjJGPkkVz1K9/2NZATNdDiQ3A8yGzdVJoKh9pA==} @@ -5883,8 +5887,8 @@ packages: resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} engines: {node: '>=0.6'} - qs@6.12.3: - resolution: {integrity: sha512-AWJm14H1vVaO/iNZ4/hO+HyaTehuy9nRqVdkTqlJt0HWvBiBIEXFmb4C0DGeYo3Xes9rrEW+TxHsaigCbN5ICQ==} + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} query-string@7.1.3: @@ -9766,10 +9770,11 @@ snapshots: '@sovpro/delimited-stream@1.1.0': {} - '@sphereon/did-auth-siop@0.15.1-next.4': + '@sphereon/did-auth-siop@0.16.1-next.3': dependencies: '@astronautlabs/jsonpath': 1.1.2 '@sphereon/did-uni-client': 0.6.3 + '@sphereon/oid4vc-common': 0.16.1-next.3 '@sphereon/pex': 3.3.3 '@sphereon/pex-models': 2.2.4 '@sphereon/ssi-types': 0.22.0 @@ -9779,12 +9784,11 @@ snapshots: jwt-decode: 4.0.0 language-tags: 1.0.9 multiformats: 12.1.3 - qs: 6.12.3 - sha.js: 2.4.11 + qs: 6.13.0 uint8arrays: 3.1.1 - uuid: 9.0.1 transitivePeerDependencies: - encoding + - supports-color '@sphereon/did-uni-client@0.6.3': dependencies: @@ -9793,31 +9797,44 @@ snapshots: transitivePeerDependencies: - encoding - '@sphereon/oid4vci-client@0.15.1-next.4': + '@sphereon/oid4vc-common@0.16.1-next.3': + dependencies: + '@sphereon/ssi-types': 0.28.0 + jwt-decode: 4.0.0 + sha.js: 2.4.11 + uint8arrays: 3.1.1 + uuid: 9.0.1 + transitivePeerDependencies: + - supports-color + + '@sphereon/oid4vci-client@0.16.1-next.3': dependencies: - '@sphereon/oid4vci-common': 0.15.1-next.4 - '@sphereon/ssi-types': 0.26.1-next.132 + '@sphereon/oid4vc-common': 0.16.1-next.3 + '@sphereon/oid4vci-common': 0.16.1-next.3 + '@sphereon/ssi-types': 0.28.0 cross-fetch: 3.1.8 debug: 4.3.5 transitivePeerDependencies: - encoding - supports-color - '@sphereon/oid4vci-common@0.15.1-next.4': + '@sphereon/oid4vci-common@0.16.1-next.3': dependencies: - '@sphereon/ssi-types': 0.26.1-next.132 + '@sphereon/oid4vc-common': 0.16.1-next.3 + '@sphereon/ssi-types': 0.28.0 cross-fetch: 3.1.8 jwt-decode: 4.0.0 - sha.js: 2.4.11 uint8arrays: 3.1.1 + uuid: 9.0.1 transitivePeerDependencies: - encoding - supports-color - '@sphereon/oid4vci-issuer@0.15.1-next.4': + '@sphereon/oid4vci-issuer@0.16.1-next.3': dependencies: - '@sphereon/oid4vci-common': 0.15.1-next.4 - '@sphereon/ssi-types': 0.26.1-next.132 + '@sphereon/oid4vc-common': 0.16.1-next.3 + '@sphereon/oid4vci-common': 0.16.1-next.3 + '@sphereon/ssi-types': 0.28.0 uuid: 9.0.1 transitivePeerDependencies: - encoding @@ -9845,12 +9862,7 @@ snapshots: '@sd-jwt/decode': 0.6.1 jwt-decode: 3.1.2 - '@sphereon/ssi-types@0.23.4': - dependencies: - '@sd-jwt/decode': 0.6.1 - jwt-decode: 3.1.2 - - '@sphereon/ssi-types@0.26.1-next.132': + '@sphereon/ssi-types@0.28.0': dependencies: '@sd-jwt/decode': 0.6.1 debug: 4.3.5 @@ -14122,7 +14134,7 @@ snapshots: dependencies: side-channel: 1.0.6 - qs@6.12.3: + qs@6.13.0: dependencies: side-channel: 1.0.6