From 1d794edfe5e350392f6d0cda47a176737c53de5b Mon Sep 17 00:00:00 2001 From: giovanni-riela Date: Tue, 26 May 2026 09:53:07 +0200 Subject: [PATCH 1/3] feat: implement missing rp test cases 28, 53 and 110 --- ...authorization-request.presentation.spec.ts | 94 +++++++++++++++++++ .../presentation/happy.presentation.spec.ts | 77 +++++++++++++++ 2 files changed, 171 insertions(+) diff --git a/tests/conformance/presentation/authorization-request.presentation.spec.ts b/tests/conformance/presentation/authorization-request.presentation.spec.ts index 07dcda74..4f046f63 100644 --- a/tests/conformance/presentation/authorization-request.presentation.spec.ts +++ b/tests/conformance/presentation/authorization-request.presentation.spec.ts @@ -423,6 +423,100 @@ describe(`[${testConfig.name}] Presentation Authorization Request Validation`, ( } }); + // ----------------------------------------------------------------------- + // RPR-53 — SD-JWT integrity verification failure + // ----------------------------------------------------------------------- + + test("RPR_053: SD-JWT integrity verification failure | RP rejects a response containing a tampered KB-JWT signature", async () => { + const log = baseLog.withTag("RPR_053"); + const DESCRIPTION = + "RP correctly rejected response with tampered SD-JWT KB-JWT signature"; + log.start("Conformance test: SD-JWT integrity verification failure"); + + let testSuccess = false; + try { + const authResult = await runAuthorizationStep( + testConfig.authorizeStepClass, + ); + expect(authResult.success).toBe(true); + if (!authResult.response) { + throw new Error("auth request was not successful"); + } + + const { requestObject, responseUri } = authResult.response; + const rawVpToken = authResult.response.authorizationResponse + .authorizationResponsePayload.vp_token as Record< + string, + string | string[] + >; + + log.info("→ Tampering KB-JWT signature inside the SD-JWT VP vp_token..."); + + const tamperedVpToken: Record = {}; + for (const [credId, sdJwtVp] of Object.entries(rawVpToken)) { + const tamperSdJwt = (sdJwt: string): string => { + const parts = sdJwt.split("~"); + const kbJwt = parts[parts.length - 1] ?? ""; + const jwtParts = kbJwt.split("."); + if (jwtParts.length !== 3) return sdJwt; + const sig = jwtParts[2] ?? ""; + const tamperedSig = + sig.slice(0, -4) + (sig.endsWith("AAAA") ? "BBBB" : "AAAA"); + jwtParts[2] = tamperedSig; + parts[parts.length - 1] = jwtParts.join("."); + return parts.join("~"); + }; + + tamperedVpToken[credId] = Array.isArray(sdJwtVp) + ? sdJwtVp.map(tamperSdJwt) + : tamperSdJwt(sdJwtVp); + } + + log.info( + "→ Re-building JARM with tampered vp_token and posting to response_uri...", + ); + + const metadata = { + ...verifierMetadata, + authorization_encrypted_response_alg: + verifierMetadata.authorization_encrypted_response_alg || "ECDH-ES", + authorization_encrypted_response_enc: + verifierMetadata.authorization_encrypted_response_enc || + "A128CBC-HS256", + }; + + const tamperedAuthorizationResponse = await createAuthorizationResponse({ + authorization_encrypted_response_alg: + metadata.authorization_encrypted_response_alg, + authorization_encrypted_response_enc: + metadata.authorization_encrypted_response_enc, + callbacks: { + ...partialCallbacks, + encryptJwe: getEncryptJweCallback(), + }, + config: ioWalletSdkConfig, + requestObject, + rpJwks: { jwks: metadata.jwks }, + vp_token: tamperedVpToken, + } as CreateAuthorizationResponseVersionedOptions); + + const formBody = new URLSearchParams({ + response: tamperedAuthorizationResponse.jarm.responseJwe, + }); + const response = await postToResponseUri(responseUri, { + body: formBody.toString(), + }); + + log.debug(` Response status: ${response.status}`); + log.info("→ Validating RP rejected the tampered SD-JWT..."); + expect(response.ok).toBe(false); + + testSuccess = true; + } finally { + log.testCompleted(DESCRIPTION, testSuccess); + } + }); + // ----------------------------------------------------------------------- // RPR-60 — Invalid HTTP methods // ----------------------------------------------------------------------- diff --git a/tests/conformance/presentation/happy.presentation.spec.ts b/tests/conformance/presentation/happy.presentation.spec.ts index 0524f532..e97848ea 100644 --- a/tests/conformance/presentation/happy.presentation.spec.ts +++ b/tests/conformance/presentation/happy.presentation.spec.ts @@ -1,5 +1,6 @@ /* eslint-disable max-lines-per-function */ import { definePresentationTest } from "#/config/test-metadata"; +import { postToResponseUri } from "#/helpers"; import { assertPresentationFlowSuccess } from "#/helpers/flow-assertion-helpers"; import { useTestSummary } from "#/helpers/use-test-summary"; import { extractClientIdPrefix } from "@pagopa/io-wallet-oid4vp"; @@ -225,6 +226,36 @@ describe(`[${testConfig.name}] Credential Presentation Tests`, () => { } }); + test("RPR028: response_code has sufficient entropy with at least 32 URL-safe characters.", () => { + const log = baseLog.withTag("RPR028"); + + log.start("Conformance test: Verifying response_code entropy requirements"); + + const DESCRIPTION = + "Relying Party correctly provides response_code with sufficient entropy (≥32 characters, URL-safe charset)"; + let testSuccess = false; + try { + expect(redirectUriResult.success).toBe(true); + expect(redirectUriResult.response?.responseCode).toBeDefined(); + + const responseCode = redirectUriResult.response?.responseCode ?? ""; + log.debug(` response_code: ${responseCode}`); + log.debug(` Length: ${responseCode.length} characters`); + + expect(responseCode.length).toBeGreaterThanOrEqual(32); + log.debug( + " ✅ response_code length meets minimum entropy requirement (≥32)", + ); + + expect(responseCode).toMatch(/^[a-zA-Z0-9_-]+$/); + log.debug(" ✅ response_code uses URL-safe characters only"); + + testSuccess = true; + } finally { + log.testCompleted(DESCRIPTION, testSuccess); + } + }); + test("RPR078: Wallet Attestation request correctly uses standard DCQL query.", () => { const log = baseLog.withTag("RPR078"); @@ -622,4 +653,50 @@ describe(`[${testConfig.name}] Credential Presentation Tests`, () => { log.testCompleted(DESCRIPTION, testSuccess); } }); + + test("RPR_110: HTTP 200 on valid response_uri submission | RP returns HTTP 200 for a well-formed authorization response", async () => { + const log = baseLog.withTag("RPR_110"); + const DESCRIPTION = + "RP correctly returns HTTP 200 for a valid authorization response"; + log.start( + "Conformance test: HTTP 200 on valid response_uri submission (success path)", + ); + + let testSuccess = false; + try { + log.info( + "→ Running a fresh authorization step to obtain a valid JARM and response_uri...", + ); + const ctx = await orchestrator.runThroughAuthorize(); + + const authResponse = ctx.authorizationRequestResponse.response; + if (!authResponse) { + throw new Error( + "Fresh authorization step failed: response is undefined", + ); + } + + const freshResponseUri = authResponse.responseUri; + const freshJarm = authResponse.authorizationResponse.jarm.responseJwe; + + log.info("→ Posting valid JARM to response_uri..."); + log.debug(` response_uri: ${freshResponseUri}`); + const formBody = new URLSearchParams({ response: freshJarm }); + const response = await postToResponseUri(freshResponseUri, { + body: formBody.toString(), + }); + + log.debug(` Response status: ${response.status}`); + log.info("→ Validating RP returned HTTP 200..."); + expect( + response.status, + "RP must return HTTP 200 for a valid authorization response per OID4VP spec", + ).toBe(200); + log.debug(" ✅ response_uri returned HTTP 200"); + + testSuccess = true; + } finally { + log.testCompleted(DESCRIPTION, testSuccess); + } + }); }); From ed04729674c596a58a55cab06a3dec33c173b167 Mon Sep 17 00:00:00 2001 From: giovanni-riela Date: Wed, 3 Jun 2026 16:10:41 +0200 Subject: [PATCH 2/3] fix: remove test case RPR_110 --- .../presentation/happy.presentation.spec.ts | 47 ------------------- 1 file changed, 47 deletions(-) diff --git a/tests/conformance/presentation/happy.presentation.spec.ts b/tests/conformance/presentation/happy.presentation.spec.ts index e97848ea..a81c72e7 100644 --- a/tests/conformance/presentation/happy.presentation.spec.ts +++ b/tests/conformance/presentation/happy.presentation.spec.ts @@ -1,6 +1,5 @@ /* eslint-disable max-lines-per-function */ import { definePresentationTest } from "#/config/test-metadata"; -import { postToResponseUri } from "#/helpers"; import { assertPresentationFlowSuccess } from "#/helpers/flow-assertion-helpers"; import { useTestSummary } from "#/helpers/use-test-summary"; import { extractClientIdPrefix } from "@pagopa/io-wallet-oid4vp"; @@ -653,50 +652,4 @@ describe(`[${testConfig.name}] Credential Presentation Tests`, () => { log.testCompleted(DESCRIPTION, testSuccess); } }); - - test("RPR_110: HTTP 200 on valid response_uri submission | RP returns HTTP 200 for a well-formed authorization response", async () => { - const log = baseLog.withTag("RPR_110"); - const DESCRIPTION = - "RP correctly returns HTTP 200 for a valid authorization response"; - log.start( - "Conformance test: HTTP 200 on valid response_uri submission (success path)", - ); - - let testSuccess = false; - try { - log.info( - "→ Running a fresh authorization step to obtain a valid JARM and response_uri...", - ); - const ctx = await orchestrator.runThroughAuthorize(); - - const authResponse = ctx.authorizationRequestResponse.response; - if (!authResponse) { - throw new Error( - "Fresh authorization step failed: response is undefined", - ); - } - - const freshResponseUri = authResponse.responseUri; - const freshJarm = authResponse.authorizationResponse.jarm.responseJwe; - - log.info("→ Posting valid JARM to response_uri..."); - log.debug(` response_uri: ${freshResponseUri}`); - const formBody = new URLSearchParams({ response: freshJarm }); - const response = await postToResponseUri(freshResponseUri, { - body: formBody.toString(), - }); - - log.debug(` Response status: ${response.status}`); - log.info("→ Validating RP returned HTTP 200..."); - expect( - response.status, - "RP must return HTTP 200 for a valid authorization response per OID4VP spec", - ).toBe(200); - log.debug(" ✅ response_uri returned HTTP 200"); - - testSuccess = true; - } finally { - log.testCompleted(DESCRIPTION, testSuccess); - } - }); }); From bccad03e7b8f62e97c6585428055c8c8b827804f Mon Sep 17 00:00:00 2001 From: Giovanni Riela Date: Thu, 11 Jun 2026 16:16:45 +0200 Subject: [PATCH 3/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- ...authorization-request.presentation.spec.ts | 52 ++++++++++++++----- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/tests/conformance/presentation/authorization-request.presentation.spec.ts b/tests/conformance/presentation/authorization-request.presentation.spec.ts index 4f046f63..adc0452c 100644 --- a/tests/conformance/presentation/authorization-request.presentation.spec.ts +++ b/tests/conformance/presentation/authorization-request.presentation.spec.ts @@ -453,25 +453,51 @@ describe(`[${testConfig.name}] Presentation Authorization Request Validation`, ( log.info("→ Tampering KB-JWT signature inside the SD-JWT VP vp_token..."); const tamperedVpToken: Record = {}; - for (const [credId, sdJwtVp] of Object.entries(rawVpToken)) { - const tamperSdJwt = (sdJwt: string): string => { - const parts = sdJwt.split("~"); - const kbJwt = parts[parts.length - 1] ?? ""; - const jwtParts = kbJwt.split("."); - if (jwtParts.length !== 3) return sdJwt; - const sig = jwtParts[2] ?? ""; - const tamperedSig = - sig.slice(0, -4) + (sig.endsWith("AAAA") ? "BBBB" : "AAAA"); - jwtParts[2] = tamperedSig; - parts[parts.length - 1] = jwtParts.join("."); - return parts.join("~"); - }; + let tamperedSdJwtCount = 0; + + const tamperSdJwt = (sdJwt: string): string => { + const parts = sdJwt.split("~"); + const kbJwt = parts[parts.length - 1]; + if (!kbJwt) { + throw new Error( + "Test setup failed: vp_token entry is not an SD-JWT with a KB-JWT segment", + ); + } + + const jwtParts = kbJwt.split("."); + if (jwtParts.length !== 3) { + throw new Error( + "Test setup failed: could not parse KB-JWT inside SD-JWT (expected 3 JWT parts)", + ); + } + + const sig = jwtParts[2] ?? ""; + if (sig.length < 4) { + throw new Error( + "Test setup failed: KB-JWT signature too short to tamper", + ); + } + + const tamperedSig = + sig.slice(0, -4) + (sig.endsWith("AAAA") ? "BBBB" : "AAAA"); + jwtParts[2] = tamperedSig; + parts[parts.length - 1] = jwtParts.join("."); + tamperedSdJwtCount++; + return parts.join("~"); + }; + for (const [credId, sdJwtVp] of Object.entries(rawVpToken)) { tamperedVpToken[credId] = Array.isArray(sdJwtVp) ? sdJwtVp.map(tamperSdJwt) : tamperSdJwt(sdJwtVp); } + if (tamperedSdJwtCount === 0) { + throw new Error( + "Test setup failed: no SD-JWT entries were tampered (KB-JWT signature not found)", + ); + } + log.info( "→ Re-building JARM with tampered vp_token and posting to response_uri...", );