Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 21 additions & 14 deletions packages/consumption/src/modules/openid4vc/OpenId4VcController.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DcqlValidCredential, W3cJsonCredential } from "@credo-ts/core";
import { OpenId4VciResolvedCredentialOffer, OpenId4VpResolvedAuthorizationRequest } from "@credo-ts/openid4vc";
import { VerifiableCredential } from "@nmshd/content";
import { ConsumptionBaseController } from "../../consumption/ConsumptionBaseController";
Expand Down Expand Up @@ -90,36 +91,42 @@ export class OpenId4VcController extends ConsumptionBaseController {

private async extractMatchingCredentialsFromAuthorizationRequest(authorizationRequest: OpenId4VpResolvedAuthorizationRequest): Promise<OwnIdentityAttribute[]> {
const dcqlSatisfied = authorizationRequest.dcql?.queryResult.can_be_satisfied ?? false;
const authorizationRequestSatisfied = authorizationRequest.presentationExchange?.credentialsForRequest.areRequirementsSatisfied ?? false;
if (!dcqlSatisfied && !authorizationRequestSatisfied) {
const pexSatisfied = authorizationRequest.presentationExchange?.credentialsForRequest.areRequirementsSatisfied ?? false;
if (!dcqlSatisfied && !pexSatisfied) {
return [];
}

// there is no easy method to check which credentials were used in dcql
// this has to be added later
if (!authorizationRequestSatisfied) return [];

const matchedCredentialsFromPresentationExchange = authorizationRequest.presentationExchange?.credentialsForRequest.requirements
.map((entry) => entry.submissionEntry.map((subEntry) => subEntry.verifiableCredentials.map((vc) => vc.credentialRecord.encoded)).flat())
.flat();
let matchedCredentials: (string | W3cJsonCredential)[] = [];
if (dcqlSatisfied) {
const queryId = authorizationRequest.dcql!.queryResult.credentials[0].id; // assume there is only one query for now
const queryResult = authorizationRequest.dcql!.queryResult.credential_matches[queryId];
if (queryResult.success) {
matchedCredentials = queryResult.valid_credentials.map((vc: DcqlValidCredential) => vc.record.encoded).flat();
}
} else if (pexSatisfied) {
matchedCredentials = authorizationRequest
.presentationExchange!.credentialsForRequest.requirements.map((entry) =>
entry.submissionEntry.map((subEntry) => subEntry.verifiableCredentials.map((vc) => vc.credentialRecord.encoded)).flat()
)
.flat();
}

const allCredentials = (await this.parent.attributes.getLocalAttributes({
"@type": "OwnIdentityAttribute",
"content.value.@type": "VerifiableCredential"
})) as OwnIdentityAttribute[];

const matchingCredentials = allCredentials.filter((credential) =>
matchedCredentialsFromPresentationExchange?.includes((credential.content.value as VerifiableCredential).value as string)
); // in current demo scenarios this is a string
const matchingCredentials = allCredentials.filter((credential) => matchedCredentials.includes((credential.content.value as VerifiableCredential).value as string)); // in current demo scenarios this is a string
return matchingCredentials;
}

public async acceptAuthorizationRequest(
authorizationRequest: OpenId4VpResolvedAuthorizationRequest
authorizationRequest: OpenId4VpResolvedAuthorizationRequest,
credential: OwnIdentityAttribute
): Promise<{ status: number; message: string | Record<string, unknown> | null }> {
// parse the credential type to be sdjwt

const serverResponse = await this.holder.acceptAuthorizationRequest(authorizationRequest);
const serverResponse = await this.holder.acceptAuthorizationRequest(authorizationRequest, credential);
if (!serverResponse) throw new Error("No response from server");

return { status: serverResponse.status, message: serverResponse.body };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export class EnmeshedStorageService<T extends BaseRecord> implements StorageServ

return attributes.map((attribute) => {
const attributeValue = attribute.content.value as VerifiableCredential;
return this.fromEncoded(correspondingCredentialType, attributeValue.value) as T;
return EnmeshedStorageService.fromEncoded(correspondingCredentialType, attributeValue.value) as T;
});
}

Expand All @@ -106,7 +106,7 @@ export class EnmeshedStorageService<T extends BaseRecord> implements StorageServ
}
}

private fromEncoded(type: string, encoded: string | Record<string, any>): BaseRecord<any, any> {
public static fromEncoded(type: string, encoded: string | Record<string, any>): BaseRecord<any, any> {
switch (type) {
case ClaimFormat.SdJwtDc:
return new SdJwtVcRecord({ credentialInstances: [{ compactSdJwtVc: encoded as string }] });
Expand Down
96 changes: 50 additions & 46 deletions packages/consumption/src/modules/openid4vc/local/Holder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import { BaseRecord, ClaimFormat, DidJwk, DidKey, InjectionSymbols, JwkDidCreateOptions, KeyDidCreateOptions, Kms, MdocRecord, SdJwtVcRecord, X509Module } from "@credo-ts/core";
import {
BaseRecord,
ClaimFormat,
DcqlCredentialsForRequest,
DidJwk,
DidKey,
DifPexInputDescriptorToCredentials,
InjectionSymbols,
JwkDidCreateOptions,
KeyDidCreateOptions,
Kms,
X509Module
} from "@credo-ts/core";
import { OpenId4VciCredentialResponse, OpenId4VcModule, type OpenId4VciResolvedCredentialOffer, type OpenId4VpResolvedAuthorizationRequest } from "@credo-ts/openid4vc";
import { VerifiableCredential } from "@nmshd/content";
import { AccountController } from "@nmshd/transport";
import { AttributesController, OwnIdentityAttribute } from "../../attributes";
import { BaseAgent } from "./BaseAgent";
Expand All @@ -13,7 +26,7 @@ function getOpenIdHolderModules() {
x509: new X509Module({
getTrustedCertificatesForVerification: (_agentContext, { certificateChain, verification }) => {
// eslint-disable-next-line no-console
console.log(`dyncamically trusting certificate ${certificateChain[0].getIssuerNameField("C")} for verification of ${verification.type}`);
console.log(`dynamically trusting certificate ${certificateChain[0].getIssuerNameField("C")} for verification of ${verification.type}`);
return [certificateChain[0].toString("pem")];
}
})
Expand Down Expand Up @@ -131,7 +144,10 @@ export class Holder extends BaseAgent<ReturnType<typeof getOpenIdHolderModules>>
return resolvedRequest;
}

public async acceptAuthorizationRequest(resolvedAuthenticationRequest: OpenId4VpResolvedAuthorizationRequest): Promise<
public async acceptAuthorizationRequest(
resolvedAuthorizationRequest: OpenId4VpResolvedAuthorizationRequest,
credential: OwnIdentityAttribute
): Promise<
| {
readonly status: number;
readonly body: string | Record<string, unknown> | null;
Expand All @@ -142,56 +158,44 @@ export class Holder extends BaseAgent<ReturnType<typeof getOpenIdHolderModules>>
}
| undefined
> {
if (!resolvedAuthenticationRequest.presentationExchange && !resolvedAuthenticationRequest.dcql) {
if (!resolvedAuthorizationRequest.presentationExchange && !resolvedAuthorizationRequest.dcql) {
throw new Error("Missing presentation exchange or dcql on resolved authorization request");
}
const credentialContent = credential.content.value as VerifiableCredential;
const credentialRecord = EnmeshedStorageService.fromEncoded(credentialContent.type, credentialContent.value);

let credentialForPex: DifPexInputDescriptorToCredentials | undefined;
if (resolvedAuthorizationRequest.presentationExchange) {
const inputDescriptor = resolvedAuthorizationRequest.presentationExchange.credentialsForRequest.requirements[0].submissionEntry[0].inputDescriptorId;
credentialForPex = {
[inputDescriptor]: [
{
credentialRecord,
claimFormat: credentialContent.type as any,
disclosedPayload: {} // TODO: implement SD properly
}
]
} as any;
}

// This fix ensures that the credential records which have been loaded here actually do provide the encoded() method
// this issue arises as the records are loaded and then communicated to the app as a json object, losing the class prototype
if (resolvedAuthenticationRequest.presentationExchange) {
for (const requirementKey in resolvedAuthenticationRequest.presentationExchange.credentialsForRequest.requirements) {
const requirement = resolvedAuthenticationRequest.presentationExchange.credentialsForRequest.requirements[requirementKey];
for (const submissionEntry of requirement.submissionEntry) {
for (const vc of submissionEntry.verifiableCredentials) {
if (vc.claimFormat === ClaimFormat.SdJwtDc) {
const recordUncast = vc.credentialRecord;
const record = new SdJwtVcRecord({
id: recordUncast.id,
createdAt: recordUncast.createdAt,
credentialInstances: [{ compactSdJwtVc: recordUncast.encoded }]
});
vc.credentialRecord = record;
} else if (vc.claimFormat === ClaimFormat.MsoMdoc) {
const recordUncast = vc.credentialRecord;
const record = new MdocRecord({
id: recordUncast.id,
createdAt: recordUncast.createdAt,
credentialInstances: [{ issuerSignedBase64Url: recordUncast.encoded }]
});
vc.credentialRecord = record;
} else {
// eslint-disable-next-line no-console
console.log("Unsupported credential format in demo app, only sd-jwt-vc is supported at the moment");
}
let credentialForDcql: DcqlCredentialsForRequest | undefined;
if (resolvedAuthorizationRequest.dcql) {
const queryId = resolvedAuthorizationRequest.dcql.queryResult.credentials[0].id;
credentialForDcql = {
[queryId]: [
{
credentialRecord,
claimFormat: credentialContent.type as any,
disclosedPayload: {} // TODO: implement SD properly
}
}
}
]
} as any;
}

const submissionResult = await this.agent.openid4vc.holder.acceptOpenId4VpAuthorizationRequest({
authorizationRequestPayload: resolvedAuthenticationRequest.authorizationRequestPayload,
presentationExchange: resolvedAuthenticationRequest.presentationExchange
? {
credentials: this.agent.openid4vc.holder.selectCredentialsForPresentationExchangeRequest(
resolvedAuthenticationRequest.presentationExchange.credentialsForRequest
)
}
: undefined,
dcql: resolvedAuthenticationRequest.dcql
? {
credentials: this.agent.openid4vc.holder.selectCredentialsForDcqlRequest(resolvedAuthenticationRequest.dcql.queryResult)
}
: undefined
authorizationRequestPayload: resolvedAuthorizationRequest.authorizationRequestPayload,
presentationExchange: credentialForPex ? { credentials: credentialForPex } : undefined,
dcql: credentialForDcql ? { credentials: credentialForDcql } : undefined
});
return submissionResult.serverResponse;
}
Expand Down
4 changes: 4 additions & 0 deletions packages/runtime/src/useCases/common/Schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17210,9 +17210,13 @@ export const AcceptAuthorizationRequestRequest: any = {
"properties": {
"authorizationRequest": {
"type": "object"
},
"attributeId": {
"type": "string"
}
},
"required": [
"attributeId",
"authorizationRequest"
]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { OpenId4VpResolvedAuthorizationRequest } from "@credo-ts/openid4vc";
import { Result } from "@js-soft/ts-utils";
import { OpenId4VcController } from "@nmshd/consumption";
import { AttributesController, LocalAttribute, OpenId4VcController, OwnIdentityAttribute } from "@nmshd/consumption";
import { CoreId } from "@nmshd/core-types";
import { Inject } from "@nmshd/typescript-ioc";
import { SchemaRepository, SchemaValidator, UseCase } from "../../common";
import { RuntimeErrors, SchemaRepository, SchemaValidator, UseCase } from "../../common";

export interface AbstractAcceptAuthorizationRequestRequest<T> {
authorizationRequest: T;
attributeId: string;
}

export interface AcceptAuthorizationRequestRequest extends AbstractAcceptAuthorizationRequestRequest<OpenId4VpResolvedAuthorizationRequest> {}
Expand All @@ -26,13 +28,17 @@ class Validator extends SchemaValidator<AcceptAuthorizationRequestRequest> {
export class AcceptAuthorizationRequestUseCase extends UseCase<AcceptAuthorizationRequestRequest, AcceptAuthorizationRequestResponse> {
public constructor(
@Inject private readonly openId4VcController: OpenId4VcController,
@Inject private readonly attributesController: AttributesController,
@Inject validator: Validator
) {
super(validator);
}

protected override async executeInternal(request: AcceptAuthorizationRequestRequest): Promise<Result<AcceptAuthorizationRequestResponse>> {
const result = await this.openId4VcController.acceptAuthorizationRequest(request.authorizationRequest);
const credential = (await this.attributesController.getLocalAttribute(CoreId.from(request.attributeId))) as OwnIdentityAttribute | undefined;
if (!credential) return Result.fail(RuntimeErrors.general.recordNotFound(LocalAttribute));

const result = await this.openId4VcController.acceptAuthorizationRequest(request.authorizationRequest, OwnIdentityAttribute.from(credential));
return Result.ok({ status: result.status, message: JSON.stringify(result.message) });
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DcqlValidCredential } from "@credo-ts/core";
import { OpenId4VpResolvedAuthorizationRequest } from "@credo-ts/openid4vc";
import { Result } from "@js-soft/ts-utils";
import { OpenId4VcController } from "@nmshd/consumption";
Expand Down Expand Up @@ -39,7 +40,7 @@ export class ResolveAuthorizationRequestUseCase extends UseCase<ResolveAuthoriza
return Result.ok({ authorizationRequest, matchingCredentials: [] });
}

// the 'get encoded' of the credential is lost while making it app-safe, we have to re-add it for PEX
// some properties are lost while making it app-safe, we have to re-add it for PEX
// quick-fix for the simplest case with one requested credential only - otherwise every [0] would have to be generalised.
if (result.authorizationRequest.presentationExchange) {
const encodedCredential =
Expand All @@ -48,6 +49,15 @@ export class ResolveAuthorizationRequestUseCase extends UseCase<ResolveAuthoriza
encodedCredential;
}

if (result.authorizationRequest.dcql) {
const queryId = result.authorizationRequest.dcql.queryResult.credentials[0].id;
const queryResult = result.authorizationRequest.dcql.queryResult.credential_matches[queryId];
if (queryResult.success) {
const recordType = (queryResult.valid_credentials[0] as DcqlValidCredential).record.type;
authorizationRequest.dcql.queryResult.credential_matches[queryId].valid_credentials[0].record.type = recordType;
}
}

return Result.ok({ authorizationRequest, matchingCredentials: AttributeMapper.toAttributeDTOList(result.matchingCredentials) });
}
}
Loading
Loading