diff --git a/.github/workflows/pull-request-Dynamo.yml b/.github/workflows/pull-request-Dynamo.yml index 58843f577..178ada972 100644 --- a/.github/workflows/pull-request-Dynamo.yml +++ b/.github/workflows/pull-request-Dynamo.yml @@ -38,7 +38,7 @@ jobs: submodules: true - name: Checks if commiting secrets to repo - uses: pre-commit/action@v3.0.0 + uses: pre-commit/action@v3.0.1 with: extra_args: "detect-secrets --all-files" diff --git a/.github/workflows/pull-request-KMS.yml b/.github/workflows/pull-request-KMS.yml index 5c2a87bcc..ac408f851 100644 --- a/.github/workflows/pull-request-KMS.yml +++ b/.github/workflows/pull-request-KMS.yml @@ -38,7 +38,7 @@ jobs: submodules: true - name: Checks if commiting secrets to repo - uses: pre-commit/action@v3.0.0 + uses: pre-commit/action@v3.0.1 with: extra_args: "detect-secrets --all-files" diff --git a/.github/workflows/pull-request-gov-notify-stub.yml b/.github/workflows/pull-request-gov-notify-stub.yml index 315364b3d..a26c23bbe 100644 --- a/.github/workflows/pull-request-gov-notify-stub.yml +++ b/.github/workflows/pull-request-gov-notify-stub.yml @@ -37,7 +37,7 @@ jobs: submodules: true - name: Checks if commiting secrets to repo - uses: pre-commit/action@v3.0.0 + uses: pre-commit/action@v3.0.1 with: extra_args: "detect-secrets --all-files" diff --git a/.github/workflows/pull-request-ipv-stub.yml b/.github/workflows/pull-request-ipv-stub.yml index 7433e1262..df4d3a5a6 100644 --- a/.github/workflows/pull-request-ipv-stub.yml +++ b/.github/workflows/pull-request-ipv-stub.yml @@ -37,7 +37,7 @@ jobs: submodules: true - name: Checks if committing secrets to repo - uses: pre-commit/action@v3.0.0 + uses: pre-commit/action@v3.0.1 with: extra_args: "detect-secrets --all-files" diff --git a/.github/workflows/pull-request-outbound-proxy.yml b/.github/workflows/pull-request-outbound-proxy.yml index 711467c7e..bd80bb7bb 100644 --- a/.github/workflows/pull-request-outbound-proxy.yml +++ b/.github/workflows/pull-request-outbound-proxy.yml @@ -38,7 +38,7 @@ jobs: submodules: true - name: Checks if committing secrets to repo - uses: pre-commit/action@v3.0.0 + uses: pre-commit/action@v3.0.1 with: extra_args: "detect-secrets --all-files" diff --git a/.github/workflows/pull-request-postoffice-mock.yml b/.github/workflows/pull-request-postoffice-mock.yml index cefd3b5a5..6a5874913 100644 --- a/.github/workflows/pull-request-postoffice-mock.yml +++ b/.github/workflows/pull-request-postoffice-mock.yml @@ -37,7 +37,7 @@ jobs: submodules: true - name: Checks if commiting secrets to repo - uses: pre-commit/action@v3.0.0 + uses: pre-commit/action@v3.0.1 with: extra_args: "detect-secrets --all-files" diff --git a/.github/workflows/pull-request-test-harness.yml b/.github/workflows/pull-request-test-harness.yml index 33f490423..c3c78fcde 100644 --- a/.github/workflows/pull-request-test-harness.yml +++ b/.github/workflows/pull-request-test-harness.yml @@ -37,7 +37,7 @@ jobs: submodules: true - name: Checks if committing secrets to repo - uses: pre-commit/action@v3.0.0 + uses: pre-commit/action@v3.0.1 with: extra_args: "detect-secrets --all-files" diff --git a/.github/workflows/pull-request-yoti-stub.yml b/.github/workflows/pull-request-yoti-stub.yml index 440fc9611..0fdd89133 100644 --- a/.github/workflows/pull-request-yoti-stub.yml +++ b/.github/workflows/pull-request-yoti-stub.yml @@ -37,7 +37,7 @@ jobs: submodules: true - name: Checks if commiting secrets to repo - uses: pre-commit/action@v3.0.0 + uses: pre-commit/action@v3.0.1 with: extra_args: "detect-secrets --all-files" diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 9ed688be6..c422d5f66 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -52,7 +52,7 @@ jobs: submodules: true - name: Checks if commiting secrets to repo - uses: pre-commit/action@v3.0.0 + uses: pre-commit/action@v3.0.1 with: extra_args: "detect-secrets --all-files" diff --git a/deploy/template.yaml b/deploy/template.yaml index afa1b2b3d..fed9f1ccf 100644 --- a/deploy/template.yaml +++ b/deploy/template.yaml @@ -999,6 +999,7 @@ Resources: TXMA_QUEUE_URL: !Ref TxMASQSQueue KMS_KEY_ARN: Fn::ImportValue: !Sub "${L2KMSStackName}-vc-signing-key" + DNSSUFFIX: !FindInMap [ EnvironmentVariables, !Ref Environment, DNSSUFFIX ] Policies: - AWSLambdaBasicExecutionRole - AWSXrayWriteOnlyAccess @@ -2701,6 +2702,7 @@ Resources: YOTIBASEURL: !FindInMap [ EnvironmentVariables, !Ref Environment, YOTIBASEURL ] KMS_KEY_ARN: Fn::ImportValue: !Sub "${L2KMSStackName}-vc-signing-key" + DNSSUFFIX: !FindInMap [ EnvironmentVariables, !Ref Environment, DNSSUFFIX ] IPV_CORE_QUEUE_URL: !Ref IPVCoreSQSQueue YOTI_SESSION_TTL_DAYS: !FindInMap [EnvironmentVariables, !Ref Environment, YOTISESSIONTTLDAYS] Policies: @@ -5801,6 +5803,10 @@ Outputs: Condition: IsNotProdLikeEnvironment Description: "F2F Test Harness" Value: !FindInMap [EnvironmentVariables, !Ref Environment, TESTHARNESSURL] + DNSSuffix: + Condition: IsNotProdLikeEnvironment + Description: "F2F DNS Suffix" + Value: !FindInMap [EnvironmentVariables, !Ref Environment, DNSSUFFIX] F2FGovNotifyURL: Condition: IsNotProdLikeEnvironment Description: "F2F Gov Notify API" @@ -5809,3 +5815,8 @@ Outputs: Condition: IsNotProdLikeEnvironment Description: "F2F Post Office Stub API" Value: !FindInMap [EnvironmentVariables, !Ref Environment, POSTOFFICESTUBAPI] + VcSigningKeyId: + Condition: IsNotProdLikeEnvironment + Description: "Signing Key used to sign VC" + Value: + Fn::ImportValue: !Sub "${L2KMSStackName}-vc-signing-key-id" \ No newline at end of file diff --git a/run-tests.sh b/run-tests.sh index f8607afa5..0087bac6c 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -19,6 +19,10 @@ export DEV_F2F_TEST_HARNESS_URL=$(remove_quotes "$CFN_F2FTestHarnessURL") export GOV_NOTIFY_API=$(remove_quotes "$CFN_F2FGovNotifyURL") # shellcheck disable=SC2154 export DEV_F2F_PO_STUB_URL=$(remove_quotes "$CFN_F2FPostOfficeStubURL") +# shellcheck disable=SC2154 +export VC_SIGNING_KEY_ID=$(remove_quotes "$CFN_VcSigningKeyId") +# shellcheck disable=SC2154 +export DNS_SUFFIX=$(remove_quotes "$CFN_DNSSuffix") cd /src; npm run test:api cp -rf results $TEST_REPORT_ABSOLUTE_DIR diff --git a/src/.env.example b/src/.env.example index ffb34757d..643d34ea8 100644 --- a/src/.env.example +++ b/src/.env.example @@ -7,3 +7,4 @@ DEV_F2F_MISSING_SUB_ACCESS_TOKEN= DEV_F2F_YOTI_STUB_URL= DEV_F2F_PO_STUB_URL= GOV_NOTIFY_API= +DNS_SUFFIX= \ No newline at end of file diff --git a/src/jest.setup.ts b/src/jest.setup.ts index 30bbb4561..a52fc899a 100644 --- a/src/jest.setup.ts +++ b/src/jest.setup.ts @@ -1,5 +1,6 @@ process.env.SESSION_TABLE = 'SESSIONTABLE' process.env.KMS_KEY_ARN = 'MYKMSKEY' +process.env.DNSSUFFIX = "DNSSUFFIX" process.env.ISSUER = 'https://XXX-c.env.account.gov.uk' process.env.TXMA_QUEUE_URL = "MYQUEUE" process.env.CLIENT_CONFIG = '[{"jwksEndpoint":"https://api.identity.account.gov.uk/.well-known/jwks.json","clientId":"ipv-core-stub","redirectUri":"http://localhost:8085/callback"}]' diff --git a/src/package-lock.json b/src/package-lock.json index 38ff955d0..0c4a4a112 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -52,7 +52,7 @@ "dotenv": "^16.3.1", "eslint": "^8.32.0", "eslint-config-airbnb-typescript": "^17.0.0", - "eslint-config-prettier": "^8.6.0", + "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^27.2.1", "eslint-plugin-jsdoc": "^39.6.8", @@ -7191,9 +7191,9 @@ } }, "node_modules/eslint-config-prettier": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.6.0.tgz", - "integrity": "sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "bin": { "eslint-config-prettier": "bin/cli.js" @@ -18883,9 +18883,9 @@ } }, "eslint-config-prettier": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.6.0.tgz", - "integrity": "sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", + "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, "requires": {} }, diff --git a/src/package.json b/src/package.json index 88391bbdd..5f2e4904d 100644 --- a/src/package.json +++ b/src/package.json @@ -67,7 +67,7 @@ "dotenv": "^16.3.1", "eslint": "^8.32.0", "eslint-config-airbnb-typescript": "^17.0.0", - "eslint-config-prettier": "^8.6.0", + "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^27.2.1", "eslint-plugin-jsdoc": "^39.6.8", diff --git a/src/services/AccessTokenRequestProcessor.ts b/src/services/AccessTokenRequestProcessor.ts index f6786a461..4d98b1d38 100644 --- a/src/services/AccessTokenRequestProcessor.ts +++ b/src/services/AccessTokenRequestProcessor.ts @@ -95,7 +95,7 @@ export class AccessTokenRequestProcessor { }; let accessToken; try { - accessToken = await this.kmsJwtAdapter.sign(jwtPayload); + accessToken = await this.kmsJwtAdapter.sign(jwtPayload, this.environmentVariables.dnsSuffix()); } catch (error) { this.logger.error("Failed to sign the accessToken Jwt", { messageCode: MessageCodes.FAILED_SIGNING_JWT }); return new Response(HttpCodesEnum.SERVER_ERROR, "Failed to sign the accessToken Jwt"); diff --git a/src/services/EnvironmentVariables.ts b/src/services/EnvironmentVariables.ts index 266f92c77..9d95683bf 100644 --- a/src/services/EnvironmentVariables.ts +++ b/src/services/EnvironmentVariables.ts @@ -40,6 +40,8 @@ export class EnvironmentVariables { private readonly KMS_KEY_ARN = process.env.KMS_KEY_ARN; + private readonly DNSSUFFIX = process.env.DNSSUFFIX; + private readonly CLIENT_CONFIG = process.env.CLIENT_CONFIG; private readonly ENCRYPTION_KEY_IDS = process.env.ENCRYPTION_KEY_IDS; @@ -213,8 +215,10 @@ export class EnvironmentVariables { !this.ISSUER || this.ISSUER.trim().length === 0 || !this.TXMA_QUEUE_URL || this.TXMA_QUEUE_URL.trim().length === 0 || !this.YOTI_KEY_SSM_PATH || this.YOTI_KEY_SSM_PATH.trim().length === 0 || - !this.YOTIBASEURL || this.YOTIBASEURL.trim().length === 0) { - logger.error("Environment variable PERSON_IDENTITY_TABLE_NAME or YOTI_SDK or YOTICALLBACKURL or ISSUER is not configured"); + !this.YOTIBASEURL || this.YOTIBASEURL.trim().length === 0 || + !this.KMS_KEY_ARN || this.KMS_KEY_ARN.trim().length === 0 || + !this.DNSSUFFIX || this.DNSSUFFIX.trim().length === 0) { + logger.error("Environment variable PERSON_IDENTITY_TABLE_NAME or YOTI_SDK or KMS_KEY_ARN or ISSUER or DNSSUFFIX is not configured"); throw new AppError(HttpCodesEnum.SERVER_ERROR, "Callback Service incorrectly configured"); } if (!this.YOTI_SESSION_TTL_DAYS || this.YOTI_SESSION_TTL_DAYS < 10) { @@ -354,6 +358,10 @@ export class EnvironmentVariables { return this.KMS_KEY_ARN; } + dnsSuffix(): any { + return this.DNSSUFFIX; + } + encryptionKeyIds(): any { return this.ENCRYPTION_KEY_IDS; } diff --git a/src/services/VerifiableCredentialService.ts b/src/services/VerifiableCredentialService.ts index caca4046a..67eb3399f 100644 --- a/src/services/VerifiableCredentialService.ts +++ b/src/services/VerifiableCredentialService.ts @@ -20,6 +20,8 @@ export class VerifiableCredentialService { readonly issuer: string; + readonly dnsSuffix: string; + private readonly kmsJwtAdapter: KmsJwtAdapter; private static instance: VerifiableCredentialService; @@ -29,11 +31,13 @@ export class VerifiableCredentialService { kmsJwtAdapter: KmsJwtAdapter, issuer: string, logger: Logger, + dnsSuffix: string, ) { this.issuer = issuer; this.tableName = tableName; this.logger = logger; this.kmsJwtAdapter = kmsJwtAdapter; + this.dnsSuffix = dnsSuffix; } static getInstance( @@ -41,9 +45,10 @@ export class VerifiableCredentialService { kmsJwtAdapter: KmsJwtAdapter, issuer: string, logger: Logger, + dnsSuffix: string, ): VerifiableCredentialService { if (!VerifiableCredentialService.instance) { - VerifiableCredentialService.instance = new VerifiableCredentialService(tableName, kmsJwtAdapter, issuer, logger); + VerifiableCredentialService.instance = new VerifiableCredentialService(tableName, kmsJwtAdapter, issuer, logger, dnsSuffix); } return VerifiableCredentialService.instance; } @@ -52,7 +57,7 @@ export class VerifiableCredentialService { try { if (result) { // Sign the VC - const signedJwt = await this.kmsJwtAdapter.sign(result); + const signedJwt = await this.kmsJwtAdapter.sign(result, this.dnsSuffix); this.logger.info({ message: "Successfully Signed Generated Verified Credential jwt" }); return signedJwt; } diff --git a/src/services/YotiSessionCompletionProcessor.ts b/src/services/YotiSessionCompletionProcessor.ts index 02c8fe234..8bed60362 100644 --- a/src/services/YotiSessionCompletionProcessor.ts +++ b/src/services/YotiSessionCompletionProcessor.ts @@ -53,7 +53,7 @@ export class YotiSessionCompletionProcessor { this.yotiService = YotiService.getInstance(this.logger, this.environmentVariables.yotiSdk(), this.environmentVariables.resourcesTtlInSeconds(), this.environmentVariables.clientSessionTokenTtlInDays(), YOTI_PRIVATE_KEY, this.environmentVariables.yotiBaseUrl()); this.f2fService = F2fService.getInstance(this.environmentVariables.sessionTable(), this.logger, createDynamoDbClient()); this.kmsJwtAdapter = new KmsJwtAdapter(this.environmentVariables.kmsKeyArn()); - this.verifiableCredentialService = VerifiableCredentialService.getInstance(this.environmentVariables.sessionTable(), this.kmsJwtAdapter, this.environmentVariables.issuer(), this.logger); + this.verifiableCredentialService = VerifiableCredentialService.getInstance(this.environmentVariables.sessionTable(), this.kmsJwtAdapter, this.environmentVariables.issuer(), this.logger, this.environmentVariables.dnsSuffix()); this.generateVerifiableCredential = GenerateVerifiableCredential.getInstance(this.logger); } diff --git a/src/tests/api/CallbackApi.test.ts b/src/tests/api/CallbackApi.test.ts index 616f1a11e..add022d30 100644 --- a/src/tests/api/CallbackApi.test.ts +++ b/src/tests/api/CallbackApi.test.ts @@ -84,7 +84,7 @@ describe("Callback API", () => { sqsMessage = await getDequeuedSqsMessage(sessionResponse.data.sub); } while (!sqsMessage); const jwtToken = sqsMessage["https://vocab.account.gov.uk/v1/credentialJWT"][0]; - validateJwtToken(jwtToken, vcResponseData, yotiMockId); + await validateJwtToken(jwtToken, vcResponseData, yotiMockId); }, 20000); @@ -108,7 +108,7 @@ describe("Callback API", () => { } while (!sqsMessage); const jwtToken = sqsMessage["https://vocab.account.gov.uk/v1/credentialJWT"][0]; - validateJwtToken(jwtToken, vcResponseData, "0000"); + await validateJwtToken(jwtToken, vcResponseData, "0000"); }, 20000); describe("F2F CRI Callback Endpoint UnHappyPath - Verifiable Credential Error", () => { diff --git a/src/tests/unit/services/EnvironmentVariables.test.ts b/src/tests/unit/services/EnvironmentVariables.test.ts index 08fae130a..03b484aa1 100644 --- a/src/tests/unit/services/EnvironmentVariables.test.ts +++ b/src/tests/unit/services/EnvironmentVariables.test.ts @@ -108,6 +108,17 @@ describe("EnvironmentVariables", () => { }); }); + describe("dnsSuffix", () => { + it("should return the value of DNSSUFFIX", () => { + process.env.DNSSUFFIX = "DNSSUFFIX"; + const envVars = new EnvironmentVariables(logger, ServicesEnum.GOV_NOTIFY_SERVICE); + + const result = envVars.dnsSuffix(); + + expect(result).toBe("DNSSUFFIX"); + }); + }); + describe("encryptionKeyIds", () => { it("should return the value of ENCRYPTION_KEY_IDS", () => { process.env.ENCRYPTION_KEY_IDS = "ENCRYPTION_KEY_IDS_VALUE"; diff --git a/src/tests/unit/services/VerifiableCredentialService.test.ts b/src/tests/unit/services/VerifiableCredentialService.test.ts index 976547500..f3d1e9256 100644 --- a/src/tests/unit/services/VerifiableCredentialService.test.ts +++ b/src/tests/unit/services/VerifiableCredentialService.test.ts @@ -24,6 +24,7 @@ describe("VerifiableCredentialService", () => { const issuer = "test-issuer"; const logger = mock(); const kmsJwtAdapter = new KmsJwtAdapter("kid"); + const dnsSuffix = "dnsSuffix123"; const credentialSubject = { "birthDate": [ @@ -99,6 +100,7 @@ describe("VerifiableCredentialService", () => { kmsJwtAdapter, issuer, logger, + dnsSuffix, ); }); @@ -144,7 +146,7 @@ describe("VerifiableCredentialService", () => { const result = await verifiableCredentialService.signGeneratedVerifiableCredentialJwt(jwt); expect(getNow).toHaveBeenCalled(); - expect(signMock).toHaveBeenCalledWith(payloadToSign); + expect(signMock).toHaveBeenCalledWith(payloadToSign, dnsSuffix); expect(result).toBe(signedJwt); }); @@ -218,7 +220,7 @@ describe("VerifiableCredentialService", () => { })) .rejects.toThrow(new AppError(HttpCodesEnum.SERVER_ERROR, "Failed to sign Jwt")); - expect(signMock).toHaveBeenCalledWith(payloadToSign); + expect(signMock).toHaveBeenCalledWith(payloadToSign, dnsSuffix); }); }); diff --git a/src/tests/utils/ApiConstants.ts b/src/tests/utils/ApiConstants.ts index 34aaaf93d..60418911e 100644 --- a/src/tests/utils/ApiConstants.ts +++ b/src/tests/utils/ApiConstants.ts @@ -8,4 +8,6 @@ export const constants = { DEV_F2F_SESSION_TABLE_NAME: "session-f2f-cri-ddb", GOV_NOTIFY_API: process.env.GOV_NOTIFY_API, DEV_F2F_PO_STUB_URL: process.env.DEV_F2F_PO_STUB_URL, + VC_SIGNING_KEY_ID: process.env.VC_SIGNING_KEY_ID, + DNS_SUFFIX: process.env.DNS_SUFFIX, }; diff --git a/src/tests/utils/ApiTestSteps.ts b/src/tests/utils/ApiTestSteps.ts index 26896841c..70d46f132 100644 --- a/src/tests/utils/ApiTestSteps.ts +++ b/src/tests/utils/ApiTestSteps.ts @@ -1,4 +1,4 @@ -import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; +import { fromNodeProviderChain } from "@aws-sdk/credential-providers"; import Ajv from "ajv"; import axios, { AxiosInstance } from "axios"; import { aws4Interceptor } from "aws4-axios"; @@ -6,11 +6,12 @@ import { XMLParser } from "fast-xml-parser"; import { ISessionItem } from "../../models/ISessionItem"; import { constants } from "../utils/ApiConstants"; import { jwtUtils } from "../../utils/JwtUtils"; +import crypto from "node:crypto"; const GOV_NOTIFY_INSTANCE = axios.create({ baseURL: constants.GOV_NOTIFY_API }); const API_INSTANCE = axios.create({ baseURL: constants.DEV_CRI_F2F_API_URL }); const YOTI_INSTANCE = axios.create({ baseURL: constants.DEV_F2F_YOTI_STUB_URL }); -const HARNESS_API_INSTANCE : AxiosInstance = axios.create({ baseURL: constants.DEV_F2F_TEST_HARNESS_URL }); +const HARNESS_API_INSTANCE: AxiosInstance = axios.create({ baseURL: constants.DEV_F2F_TEST_HARNESS_URL }); const PO_INSTANCE = axios.create({ baseURL: constants.DEV_F2F_PO_STUB_URL }); const customCredentialsProvider = { @@ -135,10 +136,10 @@ export async function callbackPost(sessionId: string | undefined, topic = "sessi } } -export async function sessionConfigurationGet(sessionId: any):Promise { +export async function sessionConfigurationGet(sessionId: any): Promise { const path = "/sessionConfiguration"; try { - const getRequest = await API_INSTANCE.get(path, { headers:{ "x-govuk-signin-session-id": sessionId } }); + const getRequest = await API_INSTANCE.get(path, { headers: { "x-govuk-signin-session-id": sessionId } }); return getRequest; } catch (error: any) { console.log(`Error response from ${path} endpoint: ${error}`); @@ -280,7 +281,7 @@ export async function getSqsEventList(folder: string, prefix: string, txmaEventS if (!contents || !contents.length) { return undefined; } - + keyList = contents.map(({ Key }) => Key); } while (contents.length < txmaEventSize); @@ -288,14 +289,14 @@ export async function getSqsEventList(folder: string, prefix: string, txmaEventS } export async function validateTxMAEventData(keyList: any, yotiMockID: any): Promise { - let i:any; + let i: any; const yotiMockIdPrefix = yotiMockID.slice(0, 2); for (i = 0; i < keyList.length; i++) { const getObjectResponse = await HARNESS_API_INSTANCE.get("/object/" + keyList[i], {}); console.log(JSON.stringify(getObjectResponse.data, null, 2)); let valid = true; if (getObjectResponse.data.event_name === "F2F_CRI_VC_ISSUED" || getObjectResponse.data.event_name === "F2F_YOTI_START") { - import("../data/" + getObjectResponse.data.event_name + "_" + yotiMockIdPrefix + "_SCHEMA.json" ) + import("../data/" + getObjectResponse.data.event_name + "_" + yotiMockIdPrefix + "_SCHEMA.json") .then((jsonSchema) => { const validate = ajv.compile(jsonSchema); valid = validate(getObjectResponse.data); @@ -310,7 +311,7 @@ export async function validateTxMAEventData(keyList: any, yotiMockID: any): Prom expect(valid).toBe(true); }); } else { - import("../data/" + getObjectResponse.data.event_name + "_SCHEMA.json" ) + import("../data/" + getObjectResponse.data.event_name + "_SCHEMA.json") .then((jsonSchema) => { const validate = ajv.compile(jsonSchema); valid = validate(getObjectResponse.data); @@ -329,10 +330,10 @@ export async function validateTxMAEventData(keyList: any, yotiMockID: any): Prom } export async function validateTxMAEvent(txmaEvent: string, keyList: any, yotiMockId: string, failedCheck: boolean, vcData: any): Promise { - let i:any; + let i: any; for (i = 0; i < keyList.length; i++) { const getObjectResponse = await HARNESS_API_INSTANCE.get("/object/" + keyList[i], {}); - + if (getObjectResponse.data.event_name === txmaEvent) { console.log(JSON.stringify(getObjectResponse.data, null, 2)); validateCriVcIssuedTxMAEvent(getObjectResponse.data, yotiMockId); @@ -372,9 +373,20 @@ export async function getDequeuedSqsMessage(prefix: string): Promise { return getObjectResponse.data; } -export function validateJwtToken(jwtToken: any, vcData: any, yotiId?: string): void { +export async function validateJwtToken(jwtToken: any, vcData: any, yotiId?: string): Promise { const [rawHead, rawBody, signature] = jwtToken.split("."); + // Validate Header + const decodedHeader = JSON.parse(jwtUtils.base64DecodeToString(rawHead.replace(/\W/g, ""))); + expect(decodedHeader.typ).toBe("JWT"); + const msgBuffer = new TextEncoder().encode(constants.VC_SIGNING_KEY_ID); + const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join(""); + expect(decodedHeader.kid).toBe("did:web:" + constants.DNS_SUFFIX + "#" + hashHex); + + // Validate Body const decodedBody = JSON.parse(jwtUtils.base64DecodeToString(rawBody.replace(/\W/g, ""))); + expect(decodedBody.jti).toBeTruthy(); // Strength Score const expecedStrengthScore = eval("vcData.s" + yotiId + ".strengthScore"); @@ -418,7 +430,7 @@ export function validateJwtToken(jwtToken: any, vcData: any, yotiId?: string): v } } -export function validateJwtTokenNamePart(jwtToken:any, givenName1:any, givenName2:any, givenName3:any, familyName:any):void { +export function validateJwtTokenNamePart(jwtToken: any, givenName1: any, givenName2: any, givenName3: any, familyName: any): void { const [rawHead, rawBody, signature] = jwtToken.split("."); const decodedBody = JSON.parse(jwtUtils.base64DecodeToString(rawBody.replace(/\W/g, ""))); expect(decodedBody.vc.credentialSubject.name[0].nameParts[0].value).toBe(givenName1); @@ -428,11 +440,11 @@ export function validateJwtTokenNamePart(jwtToken:any, givenName1:any, givenName } -export async function postAbortSession(reasion:any, sessionId:any): Promise { +export async function postAbortSession(reasion: any, sessionId: any): Promise { const path = constants.DEV_CRI_F2F_API_URL + "/abort"; console.log(path); try { - const postRequest = await API_INSTANCE.post(path, reasion, { headers:{ "x-govuk-signin-session-id": sessionId } }); + const postRequest = await API_INSTANCE.post(path, reasion, { headers: { "x-govuk-signin-session-id": sessionId } }); return postRequest; } catch (error: any) { @@ -484,7 +496,7 @@ export function validateCriVcIssuedTxMAEvent(txmaEvent: any, yotiMockId: any): a default: console.warn("Yoti Mock Id provided does not match expected list"); } -} +} export async function postPOCodeRequest(mockDelimitator: any, userData: any): Promise { const path = "/v1/locations/search"; @@ -499,7 +511,7 @@ export async function postPOCodeRequest(mockDelimitator: any, userData: any): Pr } } -function validateCriVcIssuedFailedChecks(txmaEvent: any, yotiMockId: any, vcData: any):void { +function validateCriVcIssuedFailedChecks(txmaEvent: any, yotiMockId: any, vcData: any): void { // Contra Indicators const expectedContraIndicatiors = eval("vcData.s" + yotiMockId + ".ci"); if (expectedContraIndicatiors) { @@ -518,7 +530,7 @@ function validateCriVcIssuedFailedChecks(txmaEvent: any, yotiMockId: any, vcData } } -export async function initiateUserInfo(docSelectionData:any, sessionId: string): Promise { +export async function initiateUserInfo(docSelectionData: any, sessionId: string): Promise { expect(sessionId).toBeTruthy(); const documentSelectionResponse = await postDocumentSelection(docSelectionData, sessionId); @@ -529,11 +541,11 @@ export async function initiateUserInfo(docSelectionData:any, sessionId: string): const authResponse = await authorizationGet(sessionId); expect(authResponse.status).toBe(200); - const tokenResponse = await tokenPost(authResponse.data.authorizationCode.value, authResponse.data.redirect_uri ); + const tokenResponse = await tokenPost(authResponse.data.authorizationCode.value, authResponse.data.redirect_uri); expect(tokenResponse.status).toBe(200); const userInfoResponse = await userInfoPost("Bearer " + tokenResponse.data.access_token); expect(userInfoResponse.status).toBe(202); -} +} diff --git a/src/utils/JwtUtils.ts b/src/utils/JwtUtils.ts index d1cc63f9f..c9ebf66b4 100644 --- a/src/utils/JwtUtils.ts +++ b/src/utils/JwtUtils.ts @@ -1,4 +1,5 @@ import * as jose from "node-jose"; +import crypto from "crypto"; export const jwtUtils = { @@ -28,4 +29,11 @@ export const jwtUtils = { const encoder = new TextEncoder(); return encoder.encode(value); }, + + // hash string then present output as UTF-8 encoded hexadecimal string + getHashedKid(keyId: string): string { + const kidBytes = Buffer.from(keyId, "utf8"); + const hash = crypto.createHash("sha256").update(kidBytes).digest(); + return Buffer.from(hash).toString("hex"); + }, }; diff --git a/src/utils/KmsJwtAdapter.ts b/src/utils/KmsJwtAdapter.ts index d50a59783..e81e2db9d 100644 --- a/src/utils/KmsJwtAdapter.ts +++ b/src/utils/KmsJwtAdapter.ts @@ -26,11 +26,11 @@ export class KmsJwtAdapter { this.kms = createKmsClient(); } - async sign(jwtPayload: JwtPayload): Promise { + async sign(jwtPayload: JwtPayload, dnsSuffix: string): Promise { const jwtHeader: JwtHeader = { alg: "ES256", typ: "JWT" }; const kid = this.kid.split("/").pop(); if (kid != null) { - jwtHeader.kid = kid; + jwtHeader.kid = (`did:web:${dnsSuffix}#${jwtUtils.getHashedKid(kid)}`); } const tokenComponents = { header: jwtUtils.base64Encode(JSON.stringify(jwtHeader)),