diff --git a/tests/conformance/presentation/authorization-request.presentation.spec.ts b/tests/conformance/presentation/authorization-request.presentation.spec.ts index 07dcda7..4f046f6 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 0524f53..e97848e 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); + } + }); });