diff --git a/apps/issuer/package.json b/apps/issuer/package.json index 7ae5b8bb..7bb19599 100644 --- a/apps/issuer/package.json +++ b/apps/issuer/package.json @@ -40,6 +40,7 @@ "dotenv": "^16.4.5", "express": "^4.19.2", "express-list-routes": "^1.2.1", + "joi": "^17.13.0", "jose": "^5.2.4", "passport-azure-ad": "^4.3.5", "passport-http-bearer": "^1.0.1", diff --git a/apps/issuer/src/config.ts b/apps/issuer/src/config.ts new file mode 100644 index 00000000..e32a3796 --- /dev/null +++ b/apps/issuer/src/config.ts @@ -0,0 +1,22 @@ +import Joi from 'joi'; +import 'dotenv/config'; + +/** + * Define the environment variables schema + */ +const envVarsSchema = Joi.object() + .keys({ + PORT: Joi.number().default(3000), + CONFIG_RELOAD: Joi.boolean().default(false), + ISSUER_BASE_URL: Joi.string().required(), + NODE_ENVIRONMENT: Joi.string() + .valid('development', 'production') + .default('development'), + }) + .unknown(); + +const { error, value: envVars } = envVarsSchema.validate(process.env); + +if (error) { + throw new Error(`Config validation error: ${error.message}`); +} diff --git a/apps/issuer/src/issuer.ts b/apps/issuer/src/issuer.ts index 250d0154..1c0f6822 100644 --- a/apps/issuer/src/issuer.ts +++ b/apps/issuer/src/issuer.ts @@ -5,12 +5,13 @@ import { CredentialSchema } from './types.js'; /** * The issuer class is responsible for managing the credentials and the metadata of the issuer. + * In case the CONFIG_REALOD environment variable is set, the issuer will reload the configuration every time a method is called. */ export class Issuer { /** * The metadata of the issuer. */ - private metadata: CredentialIssuerMetadataOpts; + private metadata!: CredentialIssuerMetadataOpts; /** * The credentials supported by the issuer. @@ -21,6 +22,10 @@ export class Issuer { * Creates a new instance of the issuer. */ constructor() { + this.loadConfig(); + } + + private loadConfig() { //instead of reading at the beginning, we could implement a read on demand. this.metadata = JSON.parse( readFileSync(join('templates', 'metadata.json'), 'utf-8') @@ -54,6 +59,9 @@ export class Issuer { * @returns */ getCredential(id: string) { + if (process.env.CONFIG_RELOAD) { + this.loadConfig(); + } const credential = this.credentials.get(id); if (!credential) { throw new Error(`The credential with the id ${id} is not supported.`); @@ -67,6 +75,9 @@ export class Issuer { * @returns */ getDisclosureFrame(id: string) { + if (process.env.CONFIG_RELOAD) { + this.loadConfig(); + } const credential = this.credentials.get(id); if (!credential) { throw new Error(`The credential with the id ${id} is not supported.`); @@ -78,6 +89,9 @@ export class Issuer { * Returns the metadata of the issuer. */ getMetadata() { + if (process.env.CONFIG_RELOAD) { + this.loadConfig(); + } return this.metadata; } } diff --git a/apps/issuer/src/main.ts b/apps/issuer/src/main.ts index 7e9fcfc1..fbc58299 100644 --- a/apps/issuer/src/main.ts +++ b/apps/issuer/src/main.ts @@ -1,3 +1,4 @@ +import './config.js'; import { ES256, digest, generateSalt } from '@sd-jwt/crypto-nodejs'; import { SDJwtVcInstance } from '@sd-jwt/sd-jwt-vc'; import { @@ -18,7 +19,7 @@ import { import { OID4VCIServer } from '@sphereon/oid4vci-issuer-server'; import { SdJwtDecodedVerifiableCredentialPayload } from '@sphereon/ssi-types'; import { DIDDocument } from 'did-resolver'; -import 'dotenv/config'; + import expressListRoutes from 'express-list-routes'; import { JWK, diff --git a/apps/issuer/tsconfig.json b/apps/issuer/tsconfig.json index b45706a4..925b848b 100644 --- a/apps/issuer/tsconfig.json +++ b/apps/issuer/tsconfig.json @@ -11,5 +11,5 @@ "strict": true /* Enable all strict type-checking options. */, "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, - "include": ["src/**/*"] + "include": ["src/**/*", "src/config.ts"] } diff --git a/apps/verifier/package.json b/apps/verifier/package.json index 5cd60a6e..f2ba392f 100644 --- a/apps/verifier/package.json +++ b/apps/verifier/package.json @@ -40,6 +40,7 @@ "dotenv": "^16.4.5", "express": "^4.19.2", "express-list-routes": "^1.2.1", + "joi": "^17.13.0", "jose": "^5.2.4", "passport-azure-ad": "^4.3.5", "passport-http-bearer": "^1.0.1", diff --git a/apps/verifier/src/RPManager.ts b/apps/verifier/src/RPManager.ts index e686a7d2..d071c69d 100644 --- a/apps/verifier/src/RPManager.ts +++ b/apps/verifier/src/RPManager.ts @@ -1,7 +1,6 @@ import { ES256, digest } from '@sd-jwt/crypto-nodejs'; import { EcdsaSignature, - InMemoryRPSessionManager, JWK, JWTPayload, PassBy, @@ -29,6 +28,7 @@ import { importJWK, jwtVerify } from 'jose'; import { getKeys, getPublicKey } from './keys.js'; import { EventEmitter } from 'node:events'; import { RPInstance } from './types.js'; +import { InMemoryRPSessionManager } from './session-manager.js'; // load the keys const { privateKey, publicKey } = await getKeys(); @@ -40,7 +40,9 @@ export const kid = did; // create the event emitter to listen to events. export const eventEmitter = new EventEmitter(); //TODO: implement a persistant session manager so reloads don't lose state -export const sessionManager = new InMemoryRPSessionManager(eventEmitter); +export const sessionManager = new InMemoryRPSessionManager(eventEmitter, { + // maxAgeInSeconds: 10, +}); /** * The RPManager is responsible for managing the relying parties. @@ -58,11 +60,36 @@ export class RPManager { let rp = this.rp.get(id); if (!rp) { rp = this.buildRP(id); + // checks every minute if the rp has active sessions. If there is none, the rp is removed. We want to do this so we can update the rp with new input without losing state. This approach could be improved since we are waiting around 4 minutes for the last finished request until the entries are removed. + setInterval(async () => { + this.remove(id); + }, 1000 * 60); this.rp.set(id, rp); } return rp; } + /** + * Removes a relying party. This is useful when the instance should be restarted with a new definition. + * @param id + */ + async remove(id: string, force = false) { + const rp = this.rp.get(id); + if (!rp) { + return; + } + if ( + !force && + //the limit for a session is 5 minutes, so after this a session becomes idle an can be removed. + !(await (rp.rp.sessionManager as InMemoryRPSessionManager).isIdle()) + ) { + // we have active sessions, we don't want to remove the rp. But at this point we do not know if they have already finished it. We just know they are not over the maximum defined limit (default 5 minutes). + return; + } + this.rp.delete(id); + console.log('Removed the rp'); + } + private buildRP(id: string) { // create the relying party const verifierFile = readFileSync(join('templates', `${id}.json`), 'utf-8'); diff --git a/apps/verifier/src/config.ts b/apps/verifier/src/config.ts new file mode 100644 index 00000000..2f0b4cfe --- /dev/null +++ b/apps/verifier/src/config.ts @@ -0,0 +1,22 @@ +import Joi from 'joi'; +import 'dotenv/config'; + +/** + * Define the environment variables schema + */ +const envVarsSchema = Joi.object() + .keys({ + PORT: Joi.number().default(3000), + CONFIG_RELOAD: Joi.boolean().default(false), + VERIFIER_BASE_URL: Joi.string().required(), + NODE_ENVIRONMENT: Joi.string() + .valid('development', 'production') + .default('development'), + }) + .unknown(); + +const { error, value: envVars } = envVarsSchema.validate(process.env); + +if (error) { + throw new Error(`Config validation error: ${error.message}`); +} diff --git a/apps/verifier/src/main.ts b/apps/verifier/src/main.ts index 6b64edd8..90b29094 100644 --- a/apps/verifier/src/main.ts +++ b/apps/verifier/src/main.ts @@ -1,8 +1,8 @@ +import './config.js'; import { PresentationDefinitionLocation, SupportedVersion, } from '@sphereon/did-auth-siop'; -import 'dotenv/config'; import expressListRoutes from 'express-list-routes'; import { v4 } from 'uuid'; import { expressSupport } from './server.js'; diff --git a/apps/verifier/src/session-manager.ts b/apps/verifier/src/session-manager.ts new file mode 100644 index 00000000..ea02b77a --- /dev/null +++ b/apps/verifier/src/session-manager.ts @@ -0,0 +1,467 @@ +import { + IRPSessionManager, + AuthorizationRequestState, + AuthorizationResponseState, + AuthorizationEvents, + AuthorizationEvent, + AuthorizationRequest, + AuthorizationRequestStateStatus, + AuthorizationResponse, + AuthorizationResponseStateStatus, +} from '@sphereon/did-auth-siop'; +import { EventEmitter } from 'node:events'; + +//we copied the code from the session manager to implement a function that allows to check if there are still active sessions so we can reinint the rp that should use other definitions. + +/** + * Please note that this session manager is not really meant to be used in large production settings, as it stores everything in memory! + * It also doesn't do scheduled cleanups. It runs a cleanup whenever a request or response is received. In a high-volume production setting you will want scheduled cleanups running in the background + * Since this is a low level library we have not created a full-fledged implementation. + * We suggest to create your own implementation using the event system of the library + */ +export class InMemoryRPSessionManager implements IRPSessionManager { + private readonly authorizationRequests: Record< + string, + AuthorizationRequestState + > = {}; + private readonly authorizationResponses: Record< + string, + AuthorizationResponseState + > = {}; + + // stored by hashcode + private readonly nonceMapping: Record = {}; + // stored by hashcode + private readonly stateMapping: Record = {}; + private readonly maxAgeInSeconds: number; + + private static getKeysForCorrelationId( + mapping: Record, + correlationId: string + ): number[] { + return Object.entries(mapping) + .filter((entry) => entry[1] === correlationId) + .map((filtered) => Number.parseInt(filtered[0])); + } + + public constructor( + eventEmitter: EventEmitter, + opts?: { maxAgeInSeconds?: number } + ) { + if (!eventEmitter) { + throw Error( + 'RP Session manager depends on an event emitter in the application' + ); + } + this.maxAgeInSeconds = opts?.maxAgeInSeconds ?? 5 * 60; + eventEmitter.on( + AuthorizationEvents.ON_AUTH_REQUEST_CREATED_SUCCESS, + this.onAuthorizationRequestCreatedSuccess.bind(this) + ); + eventEmitter.on( + AuthorizationEvents.ON_AUTH_REQUEST_CREATED_FAILED, + this.onAuthorizationRequestCreatedFailed.bind(this) + ); + eventEmitter.on( + AuthorizationEvents.ON_AUTH_REQUEST_SENT_SUCCESS, + this.onAuthorizationRequestSentSuccess.bind(this) + ); + eventEmitter.on( + AuthorizationEvents.ON_AUTH_REQUEST_SENT_FAILED, + this.onAuthorizationRequestSentFailed.bind(this) + ); + eventEmitter.on( + AuthorizationEvents.ON_AUTH_RESPONSE_RECEIVED_SUCCESS, + this.onAuthorizationResponseReceivedSuccess.bind(this) + ); + eventEmitter.on( + AuthorizationEvents.ON_AUTH_RESPONSE_RECEIVED_FAILED, + this.onAuthorizationResponseReceivedFailed.bind(this) + ); + eventEmitter.on( + AuthorizationEvents.ON_AUTH_RESPONSE_VERIFIED_SUCCESS, + this.onAuthorizationResponseVerifiedSuccess.bind(this) + ); + eventEmitter.on( + AuthorizationEvents.ON_AUTH_RESPONSE_VERIFIED_FAILED, + this.onAuthorizationResponseVerifiedFailed.bind(this) + ); + } + + /** + * Checks if there are entries in the session manager. If not the RP can be reinitialized in a safe way. + */ + isIdle(): Promise { + return this.cleanup().then( + () => + Object.keys(this.authorizationRequests).length === 0 && + Object.keys(this.authorizationResponses).length === 0 + ); + } + + async getRequestStateByCorrelationId( + correlationId: string, + errorOnNotFound?: boolean + ): Promise { + return await this.getFromMapping( + 'correlationId', + correlationId, + this.authorizationRequests, + errorOnNotFound + ); + } + + async getRequestStateByNonce( + nonce: string, + errorOnNotFound?: boolean + ): Promise { + return await this.getFromMapping( + 'nonce', + nonce, + this.authorizationRequests, + errorOnNotFound + ); + } + + async getRequestStateByState( + state: string, + errorOnNotFound?: boolean + ): Promise { + return await this.getFromMapping( + 'state', + state, + this.authorizationRequests, + errorOnNotFound + ); + } + + async getResponseStateByCorrelationId( + correlationId: string, + errorOnNotFound?: boolean + ): Promise { + return await this.getFromMapping( + 'correlationId', + correlationId, + this.authorizationResponses, + errorOnNotFound + ); + } + + async getResponseStateByNonce( + nonce: string, + errorOnNotFound?: boolean + ): Promise { + return await this.getFromMapping( + 'nonce', + nonce, + this.authorizationResponses, + errorOnNotFound + ); + } + + async getResponseStateByState( + state: string, + errorOnNotFound?: boolean + ): Promise { + return await this.getFromMapping( + 'state', + state, + this.authorizationResponses, + errorOnNotFound + ); + } + + private async getFromMapping( + type: 'nonce' | 'state' | 'correlationId', + value: string, + mapping: Record, + errorOnNotFound?: boolean + ): Promise { + const correlationId = await this.getCorrelationIdImpl( + type, + value, + errorOnNotFound + ); + const result = mapping[correlationId as string] as T; + if (!result && errorOnNotFound) { + throw Error( + `Could not find ${type} from correlation id ${correlationId}` + ); + } + return result; + } + + private async onAuthorizationRequestCreatedSuccess( + event: AuthorizationEvent + ): Promise { + this.cleanup().catch((error) => console.log(JSON.stringify(error))); + this.updateState( + 'request', + event, + AuthorizationRequestStateStatus.CREATED + ).catch((error) => console.log(JSON.stringify(error))); + } + + private async onAuthorizationRequestCreatedFailed( + event: AuthorizationEvent + ): Promise { + this.cleanup().catch((error) => console.log(JSON.stringify(error))); + this.updateState( + 'request', + event, + AuthorizationRequestStateStatus.ERROR + ).catch((error) => console.log(JSON.stringify(error))); + } + + private async onAuthorizationRequestSentSuccess( + event: AuthorizationEvent + ): Promise { + this.cleanup().catch((error) => console.log(JSON.stringify(error))); + this.updateState( + 'request', + event, + AuthorizationRequestStateStatus.SENT + ).catch((error) => console.log(JSON.stringify(error))); + } + + private async onAuthorizationRequestSentFailed( + event: AuthorizationEvent + ): Promise { + this.cleanup().catch((error) => console.log(JSON.stringify(error))); + this.updateState( + 'request', + event, + AuthorizationRequestStateStatus.ERROR + ).catch((error) => console.log(JSON.stringify(error))); + } + + private async onAuthorizationResponseReceivedSuccess( + event: AuthorizationEvent + ): Promise { + this.cleanup().catch((error) => console.log(JSON.stringify(error))); + await this.updateState( + 'response', + event, + AuthorizationResponseStateStatus.RECEIVED + ); + } + + private async onAuthorizationResponseReceivedFailed( + event: AuthorizationEvent + ): Promise { + this.cleanup().catch((error) => console.log(JSON.stringify(error))); + await this.updateState( + 'response', + event, + AuthorizationResponseStateStatus.ERROR + ); + } + + private async onAuthorizationResponseVerifiedFailed( + event: AuthorizationEvent + ): Promise { + await this.updateState( + 'response', + event, + AuthorizationResponseStateStatus.ERROR + ); + } + + private async onAuthorizationResponseVerifiedSuccess( + event: AuthorizationEvent + ): Promise { + await this.updateState( + 'response', + event, + AuthorizationResponseStateStatus.VERIFIED + ); + } + + public async getCorrelationIdByNonce( + nonce: string, + errorOnNotFound?: boolean + ): Promise { + return await this.getCorrelationIdImpl('nonce', nonce, errorOnNotFound); + } + + public async getCorrelationIdByState( + state: string, + errorOnNotFound?: boolean + ): Promise { + return await this.getCorrelationIdImpl('state', state, errorOnNotFound); + } + + private async getCorrelationIdImpl( + type: 'nonce' | 'state' | 'correlationId', + value: string, + errorOnNotFound?: boolean + ): Promise { + if (!value || !type) { + throw Error('No type or value provided'); + } + if (type === 'correlationId') { + return value; + } + const hash = await hashCode(value); + const correlationId = + type === 'nonce' ? this.nonceMapping[hash] : this.stateMapping[hash]; + if (!correlationId && errorOnNotFound) { + throw Error(`Could not find ${type} value for ${value}`); + } + return correlationId; + } + + private async updateMapping( + mapping: Record, + event: AuthorizationEvent, + key: string, + value: string | undefined, + allowExisting: boolean + ) { + const hash = await hashcodeForValue(event, key); + const existing = mapping[hash]; + if (existing) { + if (!allowExisting) { + throw Error( + `Mapping exists for key ${key} and we do not allow overwriting values` + ); + // biome-ignore lint/style/noUselessElse: + } else if (value && existing !== value) { + throw Error('Value changed for key'); + } + } + if (!value) { + delete mapping[hash]; + } else { + mapping[hash] = value; + } + } + + private async updateState( + type: 'request' | 'response', + event: AuthorizationEvent, + status: AuthorizationRequestStateStatus | AuthorizationResponseStateStatus + ): Promise { + if (!event) { + throw new Error('event not present'); + // biome-ignore lint/style/noUselessElse: + } else if (!event.correlationId) { + throw new Error( + `'${type} ${status}' event without correlation id received` + ); + } + try { + const eventState = { + correlationId: event.correlationId, + ...(type === 'request' ? { request: event.subject } : {}), + ...(type === 'response' ? { response: event.subject } : {}), + ...(event.error ? { error: event.error } : {}), + status, + timestamp: event.timestamp, + lastUpdated: event.timestamp, + }; + if (type === 'request') { + this.authorizationRequests[event.correlationId] = + eventState as AuthorizationRequestState; + // We do not await these + this.updateMapping( + this.nonceMapping, + event, + 'nonce', + event.correlationId, + true + ).catch((error) => console.log(JSON.stringify(error))); + this.updateMapping( + this.stateMapping, + event, + 'state', + event.correlationId, + true + ).catch((error) => console.log(JSON.stringify(error))); + } else { + this.authorizationResponses[event.correlationId] = + eventState as AuthorizationResponseState; + } + } catch (error: unknown) { + console.log(`Error in update state happened: ${error}`); + // TODO VDX-166 handle error + } + } + + async deleteStateForCorrelationId(correlationId: string) { + InMemoryRPSessionManager.cleanMappingForCorrelationId( + this.nonceMapping, + correlationId + ).catch((error) => console.log(JSON.stringify(error))); + InMemoryRPSessionManager.cleanMappingForCorrelationId( + this.stateMapping, + correlationId + ).catch((error) => console.log(JSON.stringify(error))); + delete this.authorizationRequests[correlationId]; + delete this.authorizationResponses[correlationId]; + } + private static async cleanMappingForCorrelationId( + mapping: Record, + correlationId: string + ): Promise { + const keys = InMemoryRPSessionManager.getKeysForCorrelationId( + mapping, + correlationId + ); + if (keys && keys.length > 0) { + // biome-ignore lint/complexity/noForEach: + keys.forEach((key) => delete mapping[key]); + } + } + + private async cleanup() { + const now = Date.now(); + const maxAgeInMS = this.maxAgeInSeconds * 1000; + + const cleanupCorrelations = ( + reqByCorrelationId: [ + string, + AuthorizationRequestState | AuthorizationResponseState, + ] + ) => { + const correlationId = reqByCorrelationId[0]; + const authRequest = reqByCorrelationId[1]; + if (authRequest) { + const ts = authRequest.lastUpdated || authRequest.timestamp; + if (maxAgeInMS !== 0 && now > ts + maxAgeInMS) { + this.deleteStateForCorrelationId(correlationId); + } + } + }; + + // biome-ignore lint/complexity/noForEach: + Object.entries(this.authorizationRequests).forEach((reqByCorrelationId) => { + cleanupCorrelations.call(this, reqByCorrelationId); + }); + // biome-ignore lint/complexity/noForEach: + Object.entries(this.authorizationResponses).forEach( + (resByCorrelationId) => { + cleanupCorrelations.call(this, resByCorrelationId); + } + ); + } +} + +async function hashcodeForValue( + event: AuthorizationEvent, + key: string +): Promise { + const value = (await event.subject.getMergedProperty(key)) as string; + if (!value) { + throw Error(`No value found for key ${key} in Authorization Request`); + } + return hashCode(value); +} + +function hashCode(s: string): number { + let h = 1; + for (let i = 0; i < s.length; i++) + h = (Math.imul(31, h) + s.charCodeAt(i)) | 0; + + return h; +} diff --git a/package.json b/package.json index ce8d7796..af58902e 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "@sphereon/ssi-types": "^0.22.0" }, "patchedDependencies": { - "@sphereon/pex@3.3.1": "patches/@sphereon__pex@3.3.1.patch" + "@sphereon/pex@3.3.3": "patches/@sphereon__pex@3.3.3.patch" } }, "devDependencies": { diff --git a/patches/@sphereon__pex@3.3.1.patch b/patches/@sphereon__pex@3.3.3.patch similarity index 66% rename from patches/@sphereon__pex@3.3.1.patch rename to patches/@sphereon__pex@3.3.3.patch index e6717d82..3342a268 100644 --- a/patches/@sphereon__pex@3.3.1.patch +++ b/patches/@sphereon__pex@3.3.3.patch @@ -20,6 +20,19 @@ index b749cf35e6962380cee8671bd6c5f033ce9dcb78..b6db8459009e0b00e1328b7a103886b0 }, }; presentation = Object.assign(Object.assign({}, presentation), { kbJwt }); +diff --git a/dist/browser/lib/signing/types.d.ts b/dist/browser/lib/signing/types.d.ts +index 979338af02eb6923b98642bf2f84ebca808f090a..2a9ae541faef1ef5ed269c9754a7fb0d43e391dc 100644 +--- a/dist/browser/lib/signing/types.d.ts ++++ b/dist/browser/lib/signing/types.d.ts +@@ -66,7 +66,7 @@ export interface SdJwtKbJwtInput { + }; + payload: { + iat: number; +- _sd_hash: string; ++ sd_hash: string; + nonce?: string; + }; + } diff --git a/dist/main/lib/PEX.js b/dist/main/lib/PEX.js index b749cf35e6962380cee8671bd6c5f033ce9dcb78..b6db8459009e0b00e1328b7a103886b0edb6891e 100644 --- a/dist/main/lib/PEX.js @@ -42,19 +55,23 @@ index b749cf35e6962380cee8671bd6c5f033ce9dcb78..b6db8459009e0b00e1328b7a103886b0 }, }; presentation = Object.assign(Object.assign({}, presentation), { kbJwt }); +diff --git a/dist/main/lib/signing/types.d.ts b/dist/main/lib/signing/types.d.ts +index 979338af02eb6923b98642bf2f84ebca808f090a..2a9ae541faef1ef5ed269c9754a7fb0d43e391dc 100644 +--- a/dist/main/lib/signing/types.d.ts ++++ b/dist/main/lib/signing/types.d.ts +@@ -66,7 +66,7 @@ export interface SdJwtKbJwtInput { + }; + payload: { + iat: number; +- _sd_hash: string; ++ sd_hash: string; + nonce?: string; + }; + } diff --git a/dist/module/lib/PEX.js b/dist/module/lib/PEX.js -index 65aa74adf86973e99345938c826613ddcba6ca7e..b963ec86360ac1d861583031fbea56d1d8f40731 100644 +index 65aa74adf86973e99345938c826613ddcba6ca7e..464545177f8d156ba07019e20e0e6c17e6d086ef 100644 --- a/dist/module/lib/PEX.js +++ b/dist/module/lib/PEX.js -@@ -3,7 +3,7 @@ import { Status } from './ConstraintUtils'; - import { EvaluationClientWrapper } from './evaluation'; - import { PresentationSubmissionLocation, } from './signing'; - import { PEVersion, SSITypesBuilder } from './types'; --import { calculateSdHash, definitionVersionDiscovery, getSubjectIdsAsString } from './utils'; -+import { calculatesd_hash: sdHash, definitionVersionDiscovery, getSubjectIdsAsString } from './utils'; - import { PresentationDefinitionV1VB, PresentationDefinitionV2VB, PresentationSubmissionVB, ValidationEngine } from './validation'; - /** - * This is the main interfacing class to be used by developers using the PEX library. @@ -174,7 +174,7 @@ export class PEX { // aud MUST be set by the signer or provided by e.g. SIOP/OpenID4VP lib payload: { @@ -73,3 +90,16 @@ index 65aa74adf86973e99345938c826613ddcba6ca7e..b963ec86360ac1d861583031fbea56d1 }, }; presentation = { +diff --git a/dist/module/lib/signing/types.d.ts b/dist/module/lib/signing/types.d.ts +index 979338af02eb6923b98642bf2f84ebca808f090a..2a9ae541faef1ef5ed269c9754a7fb0d43e391dc 100644 +--- a/dist/module/lib/signing/types.d.ts ++++ b/dist/module/lib/signing/types.d.ts +@@ -66,7 +66,7 @@ export interface SdJwtKbJwtInput { + }; + payload: { + iat: number; +- _sd_hash: string; ++ sd_hash: string; + nonce?: string; + }; + } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70598ccb..f2610502 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,9 @@ overrides: '@sphereon/ssi-types': ^0.22.0 patchedDependencies: - '@sphereon/pex@3.3.1': - hash: aasonhtvgrpgzz22orxg5mryvi - path: patches/@sphereon__pex@3.3.1.patch + '@sphereon/pex@3.3.3': + hash: bzkrsrlwcpq6eepymelnbd2pca + path: patches/@sphereon__pex@3.3.3.patch importers: @@ -60,7 +60,7 @@ importers: version: 0.10.3(encoding@0.1.13) '@sphereon/pex': specifier: ^3.3.3 - version: 3.3.3 + version: 3.3.3(patch_hash=bzkrsrlwcpq6eepymelnbd2pca) '@sphereon/ssi-types': specifier: ^0.22.0 version: 0.22.0 @@ -208,7 +208,7 @@ importers: version: 0.10.1(encoding@0.1.13) '@sphereon/pex': specifier: ^3.3.1 - version: 3.3.1(patch_hash=aasonhtvgrpgzz22orxg5mryvi) + version: 3.3.1 '@sphereon/ssi-types': specifier: ^0.22.0 version: 0.22.0 @@ -354,6 +354,9 @@ importers: express-list-routes: specifier: ^1.2.1 version: 1.2.1 + joi: + specifier: ^17.13.0 + version: 17.13.0 jose: specifier: ^5.2.4 version: 5.2.4 @@ -433,6 +436,9 @@ importers: express-list-routes: specifier: ^1.2.1 version: 1.2.1 + joi: + specifier: ^17.13.0 + version: 17.13.0 jose: specifier: ^5.2.4 version: 5.2.4 @@ -10810,7 +10816,7 @@ snapshots: dependencies: '@astronautlabs/jsonpath': 1.1.2 '@sphereon/did-uni-client': 0.6.2(encoding@0.1.13) - '@sphereon/pex': 3.3.1(patch_hash=aasonhtvgrpgzz22orxg5mryvi) + '@sphereon/pex': 3.3.1 '@sphereon/pex-models': 2.2.4 '@sphereon/ssi-types': 0.22.0 '@sphereon/wellknown-dids-client': 0.1.3(encoding@0.1.13) @@ -10831,7 +10837,7 @@ snapshots: dependencies: '@astronautlabs/jsonpath': 1.1.2 '@sphereon/did-uni-client': 0.6.3(encoding@0.1.13) - '@sphereon/pex': 3.3.3 + '@sphereon/pex': 3.3.3(patch_hash=bzkrsrlwcpq6eepymelnbd2pca) '@sphereon/pex-models': 2.2.4 '@sphereon/ssi-types': 0.22.0 '@sphereon/wellknown-dids-client': 0.1.3(encoding@0.1.13) @@ -10940,7 +10946,7 @@ snapshots: '@sphereon/pex-models@2.2.4': {} - '@sphereon/pex@3.3.1(patch_hash=aasonhtvgrpgzz22orxg5mryvi)': + '@sphereon/pex@3.3.1': dependencies: '@astronautlabs/jsonpath': 1.1.2 '@sd-jwt/decode': 0.6.1 @@ -10955,7 +10961,7 @@ snapshots: string.prototype.matchall: 4.0.11 uint8arrays: 3.1.1 - '@sphereon/pex@3.3.3': + '@sphereon/pex@3.3.3(patch_hash=bzkrsrlwcpq6eepymelnbd2pca)': dependencies: '@astronautlabs/jsonpath': 1.1.2 '@sd-jwt/decode': 0.6.1