Skip to content
Open
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
2 changes: 1 addition & 1 deletion .dev/compose.openid4vc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: runtime-oid4vc-tests

services:
oid4vc-service:
image: ghcr.io/js-soft/openid4vc-service:1.2.0@sha256:653358212651a992d211a187a0d405f56ae50b05d6c95bbdc37e1647fd8e6c33
image: ghcr.io/js-soft/openid4vc-service:1.2.3@sha256:935d5e1e3381974c6c29aca5a626c437c1304f6d35e9aa0f0a4ca37260cbff9d
ports:
- "9000:9000"
platform: linux/amd64
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 21 additions & 16 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,40 @@ 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) {
return [];
const pexSatisfied = authorizationRequest.presentationExchange?.credentialsForRequest.areRequirementsSatisfied ?? false;
if (!dcqlSatisfied && !pexSatisfied) return [];

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();
}

// 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();

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 decodeRecord(correspondingCredentialType, attributeValue.value) as T;
});
}

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

private fromEncoded(type: string, encoded: string | Record<string, any>): BaseRecord<any, any> {
switch (type) {
case ClaimFormat.SdJwtDc:
return new SdJwtVcRecord({ credentialInstances: [{ compactSdJwtVc: encoded as string }] });
case ClaimFormat.MsoMdoc:
return new MdocRecord({ credentialInstances: [{ issuerSignedBase64Url: encoded as string }] });
case ClaimFormat.SdJwtW3cVc:
return new W3cCredentialRecord({ credentialInstances: [{ credential: encoded as string }] });
default:
throw new Error("Credential type not supported.");
}
}

public async findByQuery(agentContext: AgentContext, recordClass: BaseRecordConstructor<T>, query: Query<T>, queryOptions?: QueryOptions): Promise<T[]> {
// so far only encountered in the credential context
agentContext.config.logger.debug(`Finding records by query ${JSON.stringify(query)} and options ${JSON.stringify(queryOptions)}`);
Expand Down Expand Up @@ -148,3 +135,16 @@ export class EnmeshedStorageService<T extends BaseRecord> implements StorageServ
});
}
}

export function decodeRecord(type: string, encoded: string | Record<string, any>): BaseRecord<any, any> {
switch (type) {
case ClaimFormat.SdJwtDc:
return new SdJwtVcRecord({ credentialInstances: [{ compactSdJwtVc: encoded as string }] });
case ClaimFormat.MsoMdoc:
return new MdocRecord({ credentialInstances: [{ issuerSignedBase64Url: encoded as string }] });
case ClaimFormat.SdJwtW3cVc:
return new W3cCredentialRecord({ credentialInstances: [{ credential: encoded as string }] });
default:
throw new Error("Credential type not supported.");
}
}
99 changes: 52 additions & 47 deletions packages/consumption/src/modules/openid4vc/local/Holder.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
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";
import { EnmeshedStorageService } from "./EnmeshedStorageService";
import { decodeRecord, EnmeshedStorageService } from "./EnmeshedStorageService";
import { KeyStorage } from "./KeyStorage";
import { OpenId4VciCredentialResponseJSON } from "./OpenId4VciCredentialResponseJSON";

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,45 @@ 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");
}

// 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");
}
const credentialContent = credential.content.value as VerifiableCredential;
const credentialRecord = decodeRecord(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;
}

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, 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