Skip to content

Commit

Permalink
lot of fixes
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <[email protected]>
  • Loading branch information
TimoGlastra committed Nov 16, 2024
1 parent 35c2c96 commit 28a63dc
Show file tree
Hide file tree
Showing 32 changed files with 1,524 additions and 2,631 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@
"jest": "^29.7.0",
"prettier": "^2.3.1",
"rxjs": "^7.8.0",
"selfsigned": "^2.4.1",
"ts-jest": "^29.1.2",
"ts-node": "^10.0.0",
"tsyringe": "^4.8.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/modules/dif-presentation-exchange/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ export * from './DifPresentationExchangeError'
export * from './DifPresentationExchangeModule'
export * from './DifPresentationExchangeService'
export * from './models'
export { extractPresentationsWithDescriptorsFromSubmission } from './utils'
export type { DifPexPresentationWithDescriptor } from './utils'
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './transform'
export * from './credentialSelection'
export * from './presentationsToCreate'
export * from './presentationSelection'
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type { SingleOrArray } from '../../../utils'
import type { W3cJsonLdVerifiableCredential, W3cJwtVerifiableCredential } from '../../vc'
import type {
DifPresentationExchangeDefinition,
DifPresentationExchangeSubmission,
VerifiablePresentation,
} from '../models'

import { default as jp } from 'jsonpath'

import { CredoError } from '../../../error'
import { MdocDeviceResponse } from '../../mdoc'
import { ClaimFormat, W3cJsonLdVerifiablePresentation, W3cJwtVerifiablePresentation } from '../../vc'

export type DifPexPresentationWithDescriptor = ReturnType<
typeof extractPresentationsWithDescriptorsFromSubmission
>[number]
export function extractPresentationsWithDescriptorsFromSubmission(
presentations: SingleOrArray<VerifiablePresentation>,
submission: DifPresentationExchangeSubmission,
definition: DifPresentationExchangeDefinition
) {
return submission.descriptor_map.map((descriptor) => {
const [presentation] = jp.query(presentations, descriptor.path) as [VerifiablePresentation | undefined]
const inputDescriptor = definition.input_descriptors.find(({ id }) => id === descriptor.id)

if (!presentation) {
throw new CredoError(
`Unable to extract presentation at path '${descriptor.path}' for submission descriptor '${descriptor.id}'`
)
}

if (!inputDescriptor) {
throw new Error(
`Unable to extract input descriptor '${descriptor.id}' from definition '${definition.id}' for submission '${submission.id}'`
)
}

if (presentation instanceof MdocDeviceResponse) {
const document = presentation.documents.find((document) => document.docType === descriptor.id)
if (!document) {
throw new Error(
`Unable to extract mdoc document with doctype '${descriptor.id}' from mdoc device response for submission '${submission.id}'.`
)
}

return {
format: ClaimFormat.MsoMdoc,
descriptor,
presentation,
credential: document,
inputDescriptor,
} as const
} else if (
presentation instanceof W3cJwtVerifiablePresentation ||
presentation instanceof W3cJsonLdVerifiablePresentation
) {
if (!descriptor.path_nested) {
throw new Error(
`Submission descriptor '${descriptor.id}' for submission '${submission.id}' has no 'path_nested' but presentation is format '${presentation.claimFormat}'`
)
}

const [verifiableCredential] = jp.query(
// Path is `$.vp.verifiableCredential[]` in case of jwt vp
presentation.claimFormat === ClaimFormat.JwtVp ? { vp: presentation } : presentation,
descriptor.path_nested.path
) as [W3cJwtVerifiableCredential | W3cJsonLdVerifiableCredential | undefined]

if (!verifiableCredential) {
throw new CredoError(
`Unable to extract credential at path '${descriptor.path_nested.path}' from presentation at path '${descriptor.path}' for submission descriptor '${descriptor.id}'`
)
}

return {
format: presentation.claimFormat,
descriptor,
presentation,
credential: verifiableCredential,
inputDescriptor,
} as const
} else {
return {
format: ClaimFormat.SdJwtVc,
descriptor,
presentation,
credential: presentation,
inputDescriptor,
} as const
}
})
}
13 changes: 10 additions & 3 deletions packages/core/src/modules/mdoc/Mdoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,14 +102,21 @@ export class Mdoc {
}

const cert = X509Certificate.fromEncodedCertificate(issuerCertificate)
const issuerKey = await getJwkFromKey(cert.publicKey)
const issuerKey = getJwkFromKey(cert.publicKey)

const alg = issuerKey.supportedSignatureAlgorithms.find(
(alg): alg is JwaSignatureAlgorithm.ES256 | JwaSignatureAlgorithm.ES384 | JwaSignatureAlgorithm.ES512 => {
(
alg
): alg is
| JwaSignatureAlgorithm.ES256
| JwaSignatureAlgorithm.ES384
| JwaSignatureAlgorithm.ES512
| JwaSignatureAlgorithm.EdDSA => {
return (
alg === JwaSignatureAlgorithm.ES256 ||
alg === JwaSignatureAlgorithm.ES384 ||
alg === JwaSignatureAlgorithm.ES512
alg === JwaSignatureAlgorithm.ES512 ||
alg === JwaSignatureAlgorithm.EdDSA
)
}
)
Expand Down
18 changes: 17 additions & 1 deletion packages/core/tests/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
ProofEventTypes,
TrustPingEventTypes,
DidsApi,
X509Api,
} from '../src'
import { Key, KeyType } from '../src/crypto'
import { DidKey } from '../src/modules/dids/methods/key'
Expand Down Expand Up @@ -759,5 +760,20 @@ export async function createDidKidVerificationMethod(agentContext: AgentContext,
const verificationMethod = didCreateResult.didState.didDocument?.dereferenceKey(kid, ['authentication'])
if (!verificationMethod) throw new Error('No verification method found')

return { did, kid, verificationMethod }
return { did, kid, verificationMethod, key: didKey.key }
}

export async function createX509Certificate(agentContext: AgentContext, dns: string, key?: Key) {
const x509 = agentContext.dependencyManager.resolve(X509Api)
const certificate = await x509.createSelfSignedCertificate({
key:
key ??
(await agentContext.wallet.createKey({
keyType: KeyType.Ed25519,
})),
name: 'C=DE',
extensions: [[{ type: 'dns', value: dns }]],
})

return { certificate, base64Certificate: certificate.toString('base64') }
}
4 changes: 2 additions & 2 deletions packages/openid4vc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
"class-transformer": "^0.5.1",
"rxjs": "^7.8.0",
"zod": "^3.23.8",
"@animo-id/oid4vci": "0.0.2-alpha-20241109173747",
"@animo-id/oauth2": "0.0.2-alpha-20241109173747"
"@animo-id/oid4vci": "0.0.2-alpha-20241116111046",
"@animo-id/oauth2": "0.0.2-alpha-20241116111046"
},
"devDependencies": {
"@credo-ts/tenants": "workspace:*",
Expand Down
13 changes: 11 additions & 2 deletions packages/openid4vc/src/openid4vc-holder/OpenId4VcHolderApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
} from './OpenId4VciHolderServiceOptions'
import type { OpenId4VcSiopAcceptAuthorizationRequestOptions } from './OpenId4vcSiopHolderServiceOptions'

import { injectable, AgentContext } from '@credo-ts/core'
import { injectable, AgentContext, DifPresentationExchangeService, DifPexCredentialsForRequest } from '@credo-ts/core'

import { OpenId4VciMetadata } from '../shared'

Expand All @@ -24,7 +24,8 @@ export class OpenId4VcHolderApi {
public constructor(
private agentContext: AgentContext,
private openId4VciHolderService: OpenId4VciHolderService,
private openId4VcSiopHolderService: OpenId4VcSiopHolderService
private openId4VcSiopHolderService: OpenId4VcSiopHolderService,
private difPresentationExchangeService: DifPresentationExchangeService
) {}

/**
Expand Down Expand Up @@ -57,6 +58,14 @@ export class OpenId4VcHolderApi {
return await this.openId4VcSiopHolderService.acceptAuthorizationRequest(this.agentContext, options)
}

/**
* Automatically select credentials from available credentials for a request. Can be called after calling
* @see resolveSiopAuthorizationRequest.
*/
public selectCredentialsForRequest(credentialsForRequest: DifPexCredentialsForRequest) {
return this.difPresentationExchangeService.selectCredentialsForRequest(credentialsForRequest)
}

public async resolveIssuerMetadata(credentialIssuer: string): Promise<OpenId4VciMetadata> {
return await this.openId4VciHolderService.resolveIssuerMetadata(this.agentContext, credentialIssuer)
}
Expand Down
37 changes: 22 additions & 15 deletions packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ import {

import { OpenId4VciCredentialFormatProfile } from '../shared'
import { getOid4vciCallbacks } from '../shared/callbacks'
import { getOfferedCredentials } from '../shared/issuerMetadataUtils'
import { getOfferedCredentials, getScopesFromCredentialConfigurationsSupported } from '../shared/issuerMetadataUtils'
import { getKeyFromDid, getSupportedJwaSignatureAlgorithms } from '../shared/utils'

import { openId4VciSupportedCredentialFormats } from './OpenId4VciHolderServiceOptions'
Expand Down Expand Up @@ -118,25 +118,28 @@ export class OpenId4VciHolderService {
resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer,
authCodeFlowOptions: OpenId4VciAuthCodeFlowOptions
): Promise<OpenId4VciResolvedAuthorizationRequest> {
// TODO: add support for scope based on metadata
const { clientId, redirectUri, scope } = authCodeFlowOptions
const { metadata, credentialOfferPayload } = resolvedCredentialOffer
const { clientId, redirectUri } = authCodeFlowOptions
const { metadata, credentialOfferPayload, offeredCredentialConfigurations } = resolvedCredentialOffer

const client = this.getClient(agentContext)

// TODO: we should already support DPoP at the PAR endpoint.
// If scope is not provided, we request scope for all offered credentials
const scope =
authCodeFlowOptions.scope ?? getScopesFromCredentialConfigurationsSupported(offeredCredentialConfigurations)

const authorizationResult = await client.initiateAuthorization({
clientId,
issuerMetadata: metadata,
credentialOffer: credentialOfferPayload,
scope: scope?.join(' '),
scope: scope.join(' '),
redirectUri,
})

if (authorizationResult.authorizationFlow === AuthorizationFlow.PresentationDuringIssuance) {
return {
authorizationFlow: AuthorizationFlow.PresentationDuringIssuance,
oid4vpRequestUrl: authorizationResult.oid4vpRequestUrl,
authSession: authorizationResult.authSession,
}
}

Expand Down Expand Up @@ -242,7 +245,6 @@ export class OpenId4VciHolderService {
// : undefined

// TODO: we should support DPoP in this request as well
// TODO: this should not return pkce (it's only needed if we fallback from auth challenge to PAR)
const { authorizationChallengeResponse } = await client.retrieveAuthorizationCodeUsingPresentation({
authSession: options.authSession,
presentationDuringIssuanceSession: options.presentationDuringIssuanceSession,
Expand Down Expand Up @@ -288,7 +290,8 @@ export class OpenId4VciHolderService {
pkceCodeVerifier: options.codeVerifier,
redirectUri: options.redirectUri,
additionalRequestPayload: {
// TODO: should we make this a param? Or should we handle it as part of client auth?
// TODO: handle it as part of client auth once we support
// assertion based client authentication
client_id: options.clientId,
},
})
Expand Down Expand Up @@ -324,7 +327,7 @@ export class OpenId4VciHolderService {
) {
const { resolvedCredentialOffer, acceptCredentialOfferOptions } = options
const { metadata, offeredCredentialConfigurations } = resolvedCredentialOffer
const { credentialConfigurationIds, credentialBindingResolver, verifyCredentialStatus } =
const { credentialConfigurationIds, credentialBindingResolver, verifyCredentialStatus, requestBatch } =
acceptCredentialOfferOptions
const client = this.getClient(agentContext)

Expand Down Expand Up @@ -377,7 +380,6 @@ export class OpenId4VciHolderService {
issuerMetadata: metadata,
accessToken: options.accessToken,
credentialConfigurationId: credentialConfigurationsToRequest[0][0],
// TODO: do we already catch the dpop from error response?
dpop: options.dpop
? await this.getDpopOptions(agentContext, {
...options.dpop,
Expand All @@ -398,8 +400,12 @@ export class OpenId4VciHolderService {
throw new CredoError('No cNonce provided and unable to acquire cNonce from the credential issuer')
}

// TODO: batch issuance should be optional
const batchSize = metadata.credentialIssuer.batch_credential_issuance?.batch_size ?? 1
// If true: use max from issuer or otherwise 1
// If number not 0: use the number
// Else: use 1
const batchSize =
requestBatch === true ? metadata.credentialIssuer.batch_credential_issuance?.batch_size ?? 1 : requestBatch || 1

for (const [offeredCredentialId, offeredCredentialConfiguration] of credentialConfigurationsToRequest) {
// Get all options for the credential request (such as which kid to use, the signature algorithm, etc)
const { jwtSigner } = await this.getCredentialRequestOptions(agentContext, {
Expand Down Expand Up @@ -518,6 +524,7 @@ export class OpenId4VciHolderService {

// Now we need to determine how the credential will be bound to us
const credentialBinding = await options.credentialBindingResolver({
agentContext,
credentialFormat: format,
signatureAlgorithms,
supportedVerificationMethods,
Expand Down Expand Up @@ -846,10 +853,10 @@ export class OpenId4VciHolderService {
if (!result.every((r) => r.result.isValid)) {
agentContext.config.logger.error('Failed to validate credentials', { result })
throw new CredoError(
`Failed to validate mdoc credential. Results = ${result
.map((r) => (r.result.isValid ? undefined : r.result.error))
`Failed to validate mdoc credential(s). \n - ${result
.map((r, i) => (r.result.isValid ? undefined : `(${i}) ${r.result.error}`))
.filter(Boolean)
.join(', ')}`
.join('\n - ')}`
)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { OpenId4VcCredentialHolderBinding } from '../shared'
import type { CredentialConfigurationSupported, CredentialOfferObject, IssuerMetadataResult } from '@animo-id/oid4vci'
import type { JwaSignatureAlgorithm, Jwk, KeyType } from '@credo-ts/core'
import type { AgentContext, JwaSignatureAlgorithm, Jwk, KeyType } from '@credo-ts/core'
import type { VerifiableCredential } from '@credo-ts/core/src/modules/dif-presentation-exchange/models/index'

import { AuthorizationFlow as OpenId4VciAuthorizationFlow } from '@animo-id/oid4vci'
Expand Down Expand Up @@ -67,6 +67,7 @@ export type OpenId4VciResolvedAuthorizationRequest =
| {
oid4vpRequestUrl: string
authorizationFlow: OpenId4VciAuthorizationFlow.PresentationDuringIssuance
authSession: string
}
| {
authorizationRequestUrl: string
Expand Down Expand Up @@ -129,7 +130,7 @@ export interface OpenId4VciRetrieveAuthorizationCodeUsingPresentationOptions {
/**
* Presentation during issuance session returned by the verifier after submitting a valid presentation
*/
presentationDuringIssuanceSession: string
presentationDuringIssuanceSession?: string
}

export interface OpenId4VciCredentialRequestOptions extends Omit<OpenId4VciAcceptCredentialOfferOptions, 'userPin'> {
Expand All @@ -156,6 +157,19 @@ export interface OpenId4VciAcceptCredentialOfferOptions {
*/
credentialConfigurationIds?: string[]

/**
* Whether to request a batch of credentials if supported by the crednetial issuer.
*
* You can also provide a number to indicate the batch size. If `true` is provided
* the max size from the credential issuer will be used.
*
* If a number is passed that is higher than the max batch size of the credential issuer,
* an error will be thrown.
*
* @default false
*/
requestBatch?: boolean | number

verifyCredentialStatus?: boolean

/**
Expand Down Expand Up @@ -199,6 +213,8 @@ export interface OpenId4VciAuthCodeFlowOptions {
}

export interface OpenId4VciCredentialBindingOptions {
agentContext: AgentContext

/**
* The credential format that will be requested from the issuer.
* E.g. `jwt_vc` or `ldp_vc`.
Expand Down
Loading

0 comments on commit 28a63dc

Please sign in to comment.