From b47ccc5c9998474e9cc11442b4339328eeb4ce7f Mon Sep 17 00:00:00 2001 From: Lewis Daly Date: Fri, 17 Sep 2021 12:15:05 +0930 Subject: [PATCH] feat(int): implement verify tx integration tests (#90) * feat(int): add verifyTransaction workflow test * feat(int): add verifyTransaction workflow test * feat(int): add verifyTransaction workflow test * feat(int): add verifyTransaction workflow test * feat(int): add verifyTransaction workflow test * feat: add signature validation to the fido-lib tests * feat: succesfully recreated unit test of validation problem * feat: succesfully recreated unit test of validation problem * fix(fido): signing now working with valid creds! * fix(unit): clean up unit tests * feat: update ttk callback listening api * feat(int): finish verify transaction integration tests * fix(int): increase timeout for slower tests * chore(ci): fix typo in env var --- .circleci/config.yml | 2 +- .../thirdparty_pisp/api_spec.yaml | 135 ++++++- .../thirdparty-pisp-api-template.yaml | 9 +- .../stateMachine/registerConsent.model.ts | 2 +- .../server/workflows/registerConsent.test.ts | 9 +- .../workflows/verifyTransaction.test.ts | 234 +++++++++++ test/integration/ttkHelpers.ts | 7 + test/unit/domain/challenge.test.ts | 372 ++++++++++-------- .../verifyTransaction.model.test.ts | 3 + test/unit/shared/fido-lib.test.ts | 283 +++++++++---- 10 files changed, 793 insertions(+), 263 deletions(-) create mode 100644 test/integration/server/workflows/verifyTransaction.test.ts create mode 100644 test/integration/ttkHelpers.ts diff --git a/.circleci/config.yml b/.circleci/config.yml index 41c67e48..1b995a1b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -457,7 +457,7 @@ jobs: command: /tmp/ci-config/container-scanning/anchore-result-diff.js anchore-reports/node_12.16.1-alpine-policy.json anchore-reports/${CIRCLE_PROJECT_REPONAME}*-policy.json - slack/status: fail_only: true - webhook: "$SLACK_WEBHOOK_ANNOUNCMENT_CI_CD" + webhook: "$SLACK_WEBHOOK_ANNOUNCEMENT_CI_CD" failure_message: 'Anchore Image Scan failed for: \`"${DOCKER_ORG}/${CIRCLE_PROJECT_REPONAME}:${CIRCLE_TAG}"\`' - store_artifacts: path: anchore-reports diff --git a/docker/ml-testing-toolkit/spec_files/api_definitions/thirdparty_pisp/api_spec.yaml b/docker/ml-testing-toolkit/spec_files/api_definitions/thirdparty_pisp/api_spec.yaml index 98d8bd04..8ace83dc 100644 --- a/docker/ml-testing-toolkit/spec_files/api_definitions/thirdparty_pisp/api_spec.yaml +++ b/docker/ml-testing-toolkit/spec_files/api_definitions/thirdparty_pisp/api_spec.yaml @@ -399,6 +399,119 @@ paths: $ref: '#/components/responses/501' '503': $ref: '#/components/responses/503' + '/thirdpartyRequests/verifications/{ID}': + parameters: + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + put: + tags: + - thirdpartyRequests + - sampled + operationId: PutThirdpartyRequestsVerificationsById + summary: PutThirdpartyRequestsVerificationsById + description: > + The HTTP request `PUT /thirdpartyRequests/verifications/{ID}` is used by + the Auth-Service to inform + + the DFSP of a successful result in validating the verification of a + Thirdparty Transaction Request. + + + If the validation fails, The Auth-Service MUST use `PUT + /thirdpartyRequests/verifications/{ID}/error` + + instead. + parameters: + - $ref: '#/components/parameters/Content-Length' + requestBody: + description: The result of validating the Thirdparty Transaction Request + required: true + content: + application/json: + schema: + $ref: >- + #/components/schemas/ThirdpartyRequestsVerificationsIDPutResponse + example: + authenticationResponse: VERIFIED + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' + '/thirdpartyRequests/verifications/{ID}/error': + parameters: + - $ref: '#/components/parameters/ID' + - $ref: '#/components/parameters/Content-Type' + - $ref: '#/components/parameters/Date' + - $ref: '#/components/parameters/X-Forwarded-For' + - $ref: '#/components/parameters/FSPIOP-Source' + - $ref: '#/components/parameters/FSPIOP-Destination' + - $ref: '#/components/parameters/FSPIOP-Encryption' + - $ref: '#/components/parameters/FSPIOP-Signature' + - $ref: '#/components/parameters/FSPIOP-URI' + - $ref: '#/components/parameters/FSPIOP-HTTP-Method' + put: + tags: + - thirdpartyRequests + - sampled + operationId: PutThirdpartyRequestsVerificationsByIdAndError + summary: PutThirdpartyRequestsVerificationsByIdAndError + description: > + The HTTP request `PUT /thirdpartyRequests/verifications/{ID}/error` is + used by the Auth-Service to inform + + the DFSP of a failure in validating or looking up the verification of a + Thirdparty Transaction Request. + parameters: + - $ref: '#/components/parameters/Content-Length' + requestBody: + description: Error information returned. + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorInformationObject' + responses: + '200': + $ref: '#/components/responses/200' + '400': + $ref: '#/components/responses/400' + '401': + $ref: '#/components/responses/401' + '403': + $ref: '#/components/responses/403' + '404': + $ref: '#/components/responses/404' + '405': + $ref: '#/components/responses/405' + '406': + $ref: '#/components/responses/406' + '501': + $ref: '#/components/responses/501' + '503': + $ref: '#/components/responses/503' components: parameters: ID: @@ -783,8 +896,8 @@ components: description: | The type of the Credential. - "FIDO" - A FIDO public/private keypair - FIDOPublicKeyCredential: - title: FIDOPublicKeyCredential + FIDOPublicKeyCredentialAttestation: + title: FIDOPublicKeyCredentialAttestation type: object description: > An object sent in a `PUT /consents/{ID}` request. @@ -867,7 +980,7 @@ components: - PENDING description: The challenge has signed but not yet verified. payload: - $ref: '#/components/schemas/FIDOPublicKeyCredential' + $ref: '#/components/schemas/FIDOPublicKeyCredentialAttestation' required: - credentialType - status @@ -918,7 +1031,7 @@ components: - VERIFIED description: 'The Credential is valid, and ready to be used by the PISP.' payload: - $ref: '#/components/schemas/FIDOPublicKeyCredential' + $ref: '#/components/schemas/FIDOPublicKeyCredentialAttestation' required: - credentialType - status @@ -1222,3 +1335,17 @@ components: $ref: '#/components/schemas/ExtensionList' required: - fspId + ThirdpartyRequestsVerificationsIDPutResponse: + title: ThirdpartyRequestsVerificationsIDPutResponse + type: object + description: >- + The object sent in the PUT /thirdpartyRequests/verifications/{ID} + request. + properties: + authenticationResponse: + type: string + enum: + - VERIFIED + description: The verification passed + required: + - authenticationResponse diff --git a/docker/ml-testing-toolkit/spec_files/api_definitions/thirdparty_pisp/thirdparty-pisp-api-template.yaml b/docker/ml-testing-toolkit/spec_files/api_definitions/thirdparty_pisp/thirdparty-pisp-api-template.yaml index a822864d..8eec8895 100644 --- a/docker/ml-testing-toolkit/spec_files/api_definitions/thirdparty_pisp/thirdparty-pisp-api-template.yaml +++ b/docker/ml-testing-toolkit/spec_files/api_definitions/thirdparty_pisp/thirdparty-pisp-api-template.yaml @@ -20,8 +20,7 @@ paths: $ref: '../../../../../node_modules/@mojaloop/api-snippets/thirdparty/openapi3/paths/participants_Type_ID.yaml' # Transfer Verification Flow - # to be implemented - # /thirdpartyRequests/verifications/{ID}: - # $ref: '../../node_modules/@mojaloop/api-snippets/thirdparty/openapi3/paths/thirdpartyRequests_verifications_ID.yaml' - # /thirdpartyRequests/verifications/{ID}/error: - # $ref: '../../node_modules/@mojaloop/api-snippets/thirdparty/openapi3/paths/thirdpartyRequests_verifications_ID_error.yaml' + /thirdpartyRequests/verifications/{ID}: + $ref: '../../../../../node_modules/@mojaloop/api-snippets/thirdparty/openapi3/paths/thirdpartyRequests_verifications_ID.yaml' + /thirdpartyRequests/verifications/{ID}/error: + $ref: '../../../../../node_modules/@mojaloop/api-snippets/thirdparty/openapi3/paths/thirdpartyRequests_verifications_ID_error.yaml' diff --git a/src/domain/stateMachine/registerConsent.model.ts b/src/domain/stateMachine/registerConsent.model.ts index dfd2d535..e3fd47cb 100644 --- a/src/domain/stateMachine/registerConsent.model.ts +++ b/src/domain/stateMachine/registerConsent.model.ts @@ -125,7 +125,7 @@ export class RegisterConsentModel const challenge = deriveChallenge(consentsPostRequestAUTH) const decodedJsonString = decodeBase64String(consentsPostRequestAUTH.credential.payload.response.clientDataJSON) const parsedClientData = JSON.parse(decodedJsonString) - + const attestationExpectations: ExpectedAttestationResult = { challenge, // not sure what origin should be here diff --git a/test/integration/server/workflows/registerConsent.test.ts b/test/integration/server/workflows/registerConsent.test.ts index 5d87457e..95f2359c 100644 --- a/test/integration/server/workflows/registerConsent.test.ts +++ b/test/integration/server/workflows/registerConsent.test.ts @@ -31,17 +31,10 @@ import axios from 'axios' import headers from '~/../test/data/headers.json' import { thirdparty as tpAPI } from '@mojaloop/api-snippets' import { closeKnexConnection } from '~/model/db' +import { MLTestingToolkitRequest } from 'test/integration/ttkHelpers' const atob = require('atob') -interface MLTestingToolkitRequest { - timestamp: string - method: string - path: string - headers: Record - body: Record -} - // test data from Lewis // here is how the client should convert ArrayBuffer to base64 strings using Browser's btoa function // take a look on `reduce` version diff --git a/test/integration/server/workflows/verifyTransaction.test.ts b/test/integration/server/workflows/verifyTransaction.test.ts new file mode 100644 index 00000000..7bb37ef6 --- /dev/null +++ b/test/integration/server/workflows/verifyTransaction.test.ts @@ -0,0 +1,234 @@ + +import axios from 'axios' +import headers from '~/../test/data/headers.json' +import { thirdparty as tpAPI } from '@mojaloop/api-snippets' +import { closeKnexConnection, testCleanupConsents } from '~/model/db' +import { deriveChallenge } from '~/domain/challenge' +import { MLTestingToolkitRequest } from 'test/integration/ttkHelpers' +const atob = require('atob') +const btoa = require('btoa') + +const validConsentId = 'be433b9e-9473-4b7d-bdd5-ac5b42463afb' +const validConsentsPostRequestAuth: tpAPI.Schemas.ConsentsPostRequestAUTH = { + consentId: validConsentId, + scopes: [ + {actions: ['accounts.getBalance', 'accounts.transfer'], accountId: '412ddd18-07a0-490d-814d-27d0e9af9982'}, + {actions: ['accounts.getBalance'], accountId: '10e88508-e542-4630-be7f-bc0076029ea7'} + ], + credential: { + credentialType: 'FIDO', + status: 'PENDING', + payload: { + id: atob('vwWPva1iiTJIk_c7n9a49spEtJZBqrn4SECerci0b-Ue-6Jv9_DZo3rNX02Lq5PU4N5kGlkEPAkIoZ3499AzWQ'), + rawId: atob('vwWPva1iiTJIk/c7n9a49spEtJZBqrn4SECerci0b+Ue+6Jv9/DZo3rNX02Lq5PU4N5kGlkEPAkIoZ3499AzWQ=='), + response: { + clientDataJSON: 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiTXpnd056QTFZMkU1TlRaaFlUZzBOMlE0T1dVMFlUUTBOR1JoT1dKbFpXUmpOR1EzTlRZNU1XSTBNV0l3WldNeE9EVTJZalJoWW1Sa05EbGhORE0yTUEiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjQyMTgxIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ==', + attestationObject: 'o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIhAJEFVHrzmq90fdBVy4nOPc48vtvJVAyQleGVcp+nQ8lUAiB67XFnGhC7q7WI3NdcrCdqnewSjCfhqEvO+sbWKC60c2N4NWOBWQLBMIICvTCCAaWgAwIBAgIECwXNUzANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbjELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEnMCUGA1UEAwweWXViaWNvIFUyRiBFRSBTZXJpYWwgMTg0OTI5NjE5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIRpvsbWJJcsKwRhffCrjqLSIEBR5sR7/9VXgfZdRvSsXaiUt7lns44WZIFuz6ii/j9f8fadcBUJyrkhY5ZH8WqNsMGowIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjEwEwYLKwYBBAGC5RwCAQEEBAMCBDAwIQYLKwYBBAGC5RwBAQQEEgQQFJogIY72QTOWuIH41bfx9TAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQA+/qPfPSrgclePfgTQ3VpLaNsBr+hjLhi04LhzQxiRGWwYS+vB1TOiPXeLsQQIwbmqQU51doVbCTaXGLNIr1zvbLAwhnLWH7i9m4ahCqaCzowtTvCQ7VBUGP5T1M4eYnoo83IDCVjQj/pZG8QYgOGOigztGoWAf5CWcUF6C0UyFbONwUcqJEl2QLToa/7E8VRjm4W46IAUljYkODVZASv8h3wLROx9p5TSBlSymtwdulxQe/DKbfNSvM3edA0up+EIJKLOOU+QTR2ZQV46fEW1/ih6m8vcaY6L3NW0eYpc7TXeijUJAgoUtya/vzmnRAecuY9bncoJt8PrvL2ir2kDaGF1dGhEYXRhWMRJlg3liA6MaHQ0Fw9kdmBbj+SuuaKGMseZXPO6gx2XY0EAAAABFJogIY72QTOWuIH41bfx9QBAvwWPva1iiTJIk/c7n9a49spEtJZBqrn4SECerci0b+Ue+6Jv9/DZo3rNX02Lq5PU4N5kGlkEPAkIoZ3499AzWaUBAgMmIAEhWCAITUwire20kCqzl0A3Fbpwx2cnSqwFfTgbA2b8+a/aUiJYIHRMWJlb4Lud02oWTdQ+fejwkVo17qD0KvrwwrZZxWIg' + }, + type: 'public-key' + } + } +} + +export const validVerificationRequest: tpAPI.Schemas.ThirdpartyRequestsVerificationsPostRequest = { + verificationRequestId: '835a8444-8cdc-41ef-bf18-ca4916c2e005', + // This is stubbed out for pisp-demo-svc, where we generated these payloads + // FIDO library actually signs the base64 hash of this challenge + challenge: btoa('unimplemented123'), + consentId: 'be433b9e-9473-4b7d-bdd5-ac5b42463afb', + signedPayloadType: 'FIDO', + signedPayload: { + id: atob('vwWPva1iiTJIk_c7n9a49spEtJZBqrn4SECerci0b-Ue-6Jv9_DZo3rNX02Lq5PU4N5kGlkEPAkIoZ3499AzWQ'), + rawId: 'vwWPva1iiTJIk_c7n9a49spEtJZBqrn4SECerci0b-Ue-6Jv9_DZo3rNX02Lq5PU4N5kGlkEPAkIoZ3499AzWQ', + response: { + authenticatorData: Buffer.from([73, 150, 13, 229, 136, 14, 140, 104, 116, 52, 23, + 15, 100, 118, 96, 91, 143, 228, 174, 185, 162, 134, 50, 199, 153, 92, 243, 186, 131, 29, 151, 99, 1, 0, 0, 0, 18]).toString('base64'), + clientDataJSON: Buffer.from([123, 34, 116, 121, 112, 101, 34, 58, + 34, 119, 101, 98, 97, 117, 116, 104, 110, 46, 103, 101, 116, 34, 44, 34, 99, 104, 97, 108, 108, 101, 110, 103, 101, 34, 58, 34, 100, 87, 53, 112, 98, 88, 66, 115, 90, 87, + 49, 108, 98, 110, 82, 108, 90, 68, 69, 121, 77, 119, 34, 44, 34, 111, 114, 105, 103, 105, 110, 34, 58, 34, 104, 116, 116, 112, 58, 47, 47, 108, 111, 99, 97, 108, 104, + 111, 115, 116, 58, 52, 50, 49, 56, 49, 34, 44, 34, 99, 114, 111, 115, 115, 79, 114, 105, 103, 105, 110, 34, 58, 102, 97, 108, 115, 101, 44, 34, 111, 116, 104, 101, 114, + 95, 107, 101, 121, 115, 95, 99, 97, 110, 95, 98, 101, 95, 97, 100, 100, 101, 100, 95, 104, 101, 114, 101, 34, 58, 34, 100, 111, 32, 110, 111, 116, 32, 99, 111, 109, 112, + 97, 114, 101, 32, 99, 108, 105, 101, 110, 116, 68, 97, 116, 97, 74, 83, 79, 78, 32, 97, 103, 97, 105, 110, 115, 116, 32, 97, 32, 116, 101, 109, 112, 108, 97, 116, 101, + 46, 32, 83, 101, 101, 32, 104, 116, 116, 112, 115, 58, 47, 47, 103, 111, 111, 46, 103, 108, 47, 121, 97, 98, 80, 101, 120, 34, 125]).toString('base64'), + signature: Buffer.from([48, 68, 2, 32, 104, 17, + 39, 167, 189, 118, 136, 100, 84, 72, 120, 29, 255, 74, 131, 59, 254, 132, 36, 19, 184, 24, 93, 103, 67, 195, 25, 252, 6, 224, 120, 69, 2, 32, 56, 251, 234, 96, 138, 6, + 158, 231, 246, 168, 254, 147, 129, 142, 100, 145, 234, 99, 91, 152, 199, 15, 72, 19, 176, 237, 209, 176, 131, 243, 70, 167]).toString('base64') + }, + type: 'public-key' + } +} + +export const invalidVerificationRequest: tpAPI.Schemas.ThirdpartyRequestsVerificationsPostRequest = { + verificationRequestId: '835a8444-8cdc-41ef-bf18-ca4916c2e005', + challenge: btoa('incorrect challenge!'), + consentId: 'be433b9e-9473-4b7d-bdd5-ac5b42463afb', + signedPayloadType: 'FIDO', + signedPayload: { + id: atob('vwWPva1iiTJIk_c7n9a49spEtJZBqrn4SECerci0b-Ue-6Jv9_DZo3rNX02Lq5PU4N5kGlkEPAkIoZ3499AzWQ'), + rawId: 'vwWPva1iiTJIk_c7n9a49spEtJZBqrn4SECerci0b-Ue-6Jv9_DZo3rNX02Lq5PU4N5kGlkEPAkIoZ3499AzWQ', + response: { + authenticatorData: Buffer.from([73, 150, 13, 229, 136, 14, 140, 104, 116, 52, 23, + 15, 100, 118, 96, 91, 143, 228, 174, 185, 162, 134, 50, 199, 153, 92, 243, 186, 131, 29, 151, 99, 1, 0, 0, 0, 18]).toString('base64'), + clientDataJSON: Buffer.from([123, 34, 116, 121, 112, 101, 34, 58, + 34, 119, 101, 98, 97, 117, 116, 104, 110, 46, 103, 101, 116, 34, 44, 34, 99, 104, 97, 108, 108, 101, 110, 103, 101, 34, 58, 34, 100, 87, 53, 112, 98, 88, 66, 115, 90, 87, + 49, 108, 98, 110, 82, 108, 90, 68, 69, 121, 77, 119, 34, 44, 34, 111, 114, 105, 103, 105, 110, 34, 58, 34, 104, 116, 116, 112, 58, 47, 47, 108, 111, 99, 97, 108, 104, + 111, 115, 116, 58, 52, 50, 49, 56, 49, 34, 44, 34, 99, 114, 111, 115, 115, 79, 114, 105, 103, 105, 110, 34, 58, 102, 97, 108, 115, 101, 44, 34, 111, 116, 104, 101, 114, + 95, 107, 101, 121, 115, 95, 99, 97, 110, 95, 98, 101, 95, 97, 100, 100, 101, 100, 95, 104, 101, 114, 101, 34, 58, 34, 100, 111, 32, 110, 111, 116, 32, 99, 111, 109, 112, + 97, 114, 101, 32, 99, 108, 105, 101, 110, 116, 68, 97, 116, 97, 74, 83, 79, 78, 32, 97, 103, 97, 105, 110, 115, 116, 32, 97, 32, 116, 101, 109, 112, 108, 97, 116, 101, + 46, 32, 83, 101, 101, 32, 104, 116, 116, 112, 115, 58, 47, 47, 103, 111, 111, 46, 103, 108, 47, 121, 97, 98, 80, 101, 120, 34, 125]).toString('base64'), + signature: Buffer.from([48, 68, 2, 32, 104, 17, + 39, 167, 189, 118, 136, 100, 84, 72, 120, 29, 255, 74, 131, 59, 254, 132, 36, 19, 184, 24, 93, 103, 67, 195, 25, 252, 6, 224, 120, 69, 2, 32, 56, 251, 234, 96, 138, 6, + 158, 231, 246, 168, 254, 147, 129, 142, 100, 145, 234, 99, 91, 152, 199, 15, 72, 19, 176, 237, 209, 176, 131, 243, 70, 167]).toString('base64') + }, + type: 'public-key' + } +} + +const axiosConfig = { + headers: { + 'Content-Type': 'application/vnd.interoperability.participants+json;version=1.1', + 'Accept': 'application/vnd.interoperability.participants+json;version=1.1', + 'FSPIOP-Source': 'als', + Date: 'Thu, 24 Jan 2019 10:23:12 GMT', + 'FSPIOP-Destination': 'centralAuth' + } +} +const ttkRequestsHistoryUri = `http://localhost:5050/api/history/requests` + + +describe('POST /thirdpartyRequests/verifications', () => { + jest.setTimeout(15000) + + beforeEach(async () => { + // clear the request history in TTK between tests. + await axios.delete(ttkRequestsHistoryUri, {}) + + try { + await testCleanupConsents([ + validConsentId + ]) + } catch (err) { + // non-fatal, it's safe to ignore here. + } + }) + + afterAll(async (): Promise => { + await closeKnexConnection() + }) + + describe('happy flow', () => { + it('creates a consent, and verifies a transaction', async () => { + // check that the derivation lines up with our mock data + const derivedChallenge = deriveChallenge(validConsentsPostRequestAuth) + const preloadedChallengeFromUI = btoa('380705ca956aa847d89e4a444da9beedc4d75691b41b0ec1856b4abdd49a4360') + expect(derivedChallenge).toStrictEqual(preloadedChallengeFromUI) + + // Arrange + const consentsURI = 'http://localhost:4004/consents' + + // register the consent + const response = await axios.post(consentsURI, validConsentsPostRequestAuth, {headers}) + + // auth-service should return Accepted code + expect(response.status).toEqual(202) + + // wait a bit for the auth-service to process the request + // takes a bit since attestation takes a bit of time + await new Promise(resolve => setTimeout(resolve, 2000)) + const putParticipantsTypeIdFromALS = { + fspId: 'centralAuth' + } + const mockAlsParticipantsURI = `http://localhost:4004/participants/CONSENT/${validConsentId}` + + // mock the ALS callback to the auth-service + const responseToPutParticipantsTypeId = await axios.put(mockAlsParticipantsURI, putParticipantsTypeIdFromALS, axiosConfig) + expect(responseToPutParticipantsTypeId.status).toEqual(200) + + // // we have a registered credential - now let's try verifying a transaction + const verifyURI = 'http://localhost:4004/thirdpartyRequests/verifications' + + // Act + const result = await axios.post(verifyURI, validVerificationRequest, { headers }) + + // Assert + expect(result.status).toBe(202) + + // wait a bit for the auth-service to process the request and call the ttk + await new Promise(resolve => setTimeout(resolve, 4000)) + + // check that the auth-service has sent a PUT /thirdpartyRequests/verifications/{ID} to the DFSP (TTK) + const requestsHistory: MLTestingToolkitRequest[] = (await axios.get(ttkRequestsHistoryUri, axiosConfig)).data + const asyncCallback = requestsHistory.filter(req => { + return req.method === 'put' && req.path === `/thirdpartyRequests/verifications/${validVerificationRequest.verificationRequestId}` + }) + + expect(asyncCallback.length).toEqual(1) + + // check the payload + const asyncCallbackPayload = asyncCallback[0].body as tpAPI.Schemas.ThirdpartyRequestsVerificationsIDPutResponse + expect(asyncCallbackPayload).toStrictEqual({ + authenticationResponse: 'VERIFIED' + }) + }) + }) + + describe('unhappy flow', () => { + it('creates a consent, and tries to verify a transaction that signed the wrong challenge', async () => { + // check that the derivation lines up with our mock data + const derivedChallenge = deriveChallenge(validConsentsPostRequestAuth) + const preloadedChallengeFromUI = btoa('380705ca956aa847d89e4a444da9beedc4d75691b41b0ec1856b4abdd49a4360') + expect(derivedChallenge).toStrictEqual(preloadedChallengeFromUI) + + // Arrange + const consentsURI = 'http://localhost:4004/consents' + + // register the consent + const response = await axios.post(consentsURI, validConsentsPostRequestAuth, { headers }) + + // auth-service should return Accepted code + expect(response.status).toEqual(202) + + // wait a bit for the auth-service to process the request + // takes a bit since attestation takes a bit of time + await new Promise(resolve => setTimeout(resolve, 2000)) + const putParticipantsTypeIdFromALS = { + fspId: 'centralAuth' + } + const mockAlsParticipantsURI = `http://localhost:4004/participants/CONSENT/${validConsentId}` + + // mock the ALS callback to the auth-service + const responseToPutParticipantsTypeId = await axios.put(mockAlsParticipantsURI, putParticipantsTypeIdFromALS, axiosConfig) + expect(responseToPutParticipantsTypeId.status).toEqual(200) + + // // we have a registered credential - now let's try verifying a transaction + const verifyURI = 'http://localhost:4004/thirdpartyRequests/verifications' + + // Act + const result = await axios.post(verifyURI, invalidVerificationRequest, { headers }) + + // Assert + expect(result.status).toBe(202) + + // wait a bit for the auth-service to process the request and call the ttk + await new Promise(resolve => setTimeout(resolve, 4000)) + + // check that the auth-service has sent a PUT /thirdpartyRequests/verifications/{ID} to the DFSP (TTK) + const requestsHistory: MLTestingToolkitRequest[] = (await axios.get(ttkRequestsHistoryUri, axiosConfig)).data + const asyncCallback = requestsHistory.filter(req => { + return req.method === 'put' + && req.path === `/thirdpartyRequests/verifications/${invalidVerificationRequest.verificationRequestId}/error` + }) + + expect(asyncCallback.length).toEqual(1) + + // check the payload + const asyncCallbackPayload = asyncCallback[0].body as tpAPI.Schemas.ThirdpartyRequestsVerificationsIDPutResponse + expect(asyncCallbackPayload).toStrictEqual({ + "errorInformation": { + "errorCode": "7105", + "errorDescription": "Authorization received from PISP failed DFSP validation", + } + }) + }) + }) +}) \ No newline at end of file diff --git a/test/integration/ttkHelpers.ts b/test/integration/ttkHelpers.ts new file mode 100644 index 00000000..e5a3885a --- /dev/null +++ b/test/integration/ttkHelpers.ts @@ -0,0 +1,7 @@ +export interface MLTestingToolkitRequest { + timestamp: string + method: string + path: string + headers: Record + body: Record +} \ No newline at end of file diff --git a/test/unit/domain/challenge.test.ts b/test/unit/domain/challenge.test.ts index 7d2dbed0..45569df8 100644 --- a/test/unit/domain/challenge.test.ts +++ b/test/unit/domain/challenge.test.ts @@ -30,7 +30,10 @@ import crypto from 'crypto' import Credential from './credential' -import { verifySignature } from '~/domain/challenge' +import { deriveChallenge, verifySignature } from '~/domain/challenge' +import { canonicalize } from 'json-canonicalize' +import { thirdparty as tpAPI } from '@mojaloop/api-snippets' + /* * Signature Verification Unit Tests @@ -38,51 +41,64 @@ import { verifySignature } from '~/domain/challenge' * Currently, the tests focus on RSA 2048 and ECDSA:secp256k1 keys. * Support for additional keys can be extended further. */ -describe('Signature Verification', (): void => { - // Each test generates a random key pair - let challenge: string - let signer: crypto.Signer - - beforeEach((): void => { - challenge = 'Crypto Auth service Yay!' - // Digest Algorithm - signer = crypto.createSign('SHA256') - - // Hash challenge using SHA256 - signer.update(challenge) - }) - - it('verifies correct signature - EC Key (secp256k1)', (): void => { - const keyPair = crypto.generateKeyPairSync('ec', { - namedCurve: 'secp256k1', // Allowed by FIDO spec - publicKeyEncoding: { - type: 'spki', // Key infrasructure - format: 'pem' // Encoding format - }, - privateKeyEncoding: { - type: 'pkcs8', - format: 'pem' +describe('challenge', (): void => { + + describe('deriveChallenge', () => { + it('canonicalizes a consent the same as the flutter library', () => { + // Arrange + const rawChallenge = { + consentId: "d194d840-97e5-44e7-84cc-bc54a51a7771", + scopes: [ + { + accountId: "ba32b791-27af-4fe5-987f-f1a055031389", + actions: ["accounts.getBalance", "accounts.transfer"] + }, + { + accountId: "232b396c-edba-4d10-b83e-b2d8e938d0e9", + actions: ["accounts.getBalance"] + } + ] } + const expected = '{"consentId":"d194d840-97e5-44e7-84cc-bc54a51a7771","scopes":[{"accountId":"ba32b791-27af-4fe5-987f-f1a055031389","actions":["accounts.getBalance","accounts.transfer"]},{"accountId":"232b396c-edba-4d10-b83e-b2d8e938d0e9","actions":["accounts.getBalance"]}]}' + + // Act + const canonicalString = canonicalize(rawChallenge) + + // Assert + console.log('canonicalString is', canonicalString) + expect(canonicalString).toStrictEqual(expected) }) - const signature = signer.sign(keyPair.privateKey, 'base64') - const verified = verifySignature(challenge, signature, keyPair.publicKey) - - expect(verified).toEqual(true) + it('parses the same hash', () => { + // Arrange + const consent = { "consentId": "11a91835-cdda-418b-9c0a-e8de62fbc84c", "scopes": [{ "accountId": "a84cd5b8-5883-4deb-9dec-2e86a9603922", "actions": ["accounts.getBalance", "accounts.transfer"] }, { "accountId": "b7b40dd7-ae6b-4904-9654-82d02544b327", "actions": ["accounts.getBalance"] }] } + const expected = 'MWVhZWJlNTAzNmEyYWE4NjJkN2UxNmM3ODkzYjc4ZjNjOGRiOWFjOGNhOTY1ZDhjMGQ5OTRlMDcxODU3YjVjZQ==' + + // Act + const result = deriveChallenge(consent as unknown as tpAPI.Schemas.ConsentsPostRequestAUTH) + + // Assert + console.log('result is', result) + expect(result).toStrictEqual(expected) + }) }) - // Using a correct hardcoded key, challenge and signature triplet - it('verifies correct signature - hardcoded EC Key (secp256k1)', (): void => { - const { message, signature, keyPair } = Credential.EC + describe('verifySignature', () => { + // Each test generates a random key pair + let challenge: string + let signer: crypto.Signer - const verified = verifySignature(message, signature, keyPair.public) + beforeEach((): void => { + challenge = 'Crypto Auth service Yay!' + // Digest Algorithm + signer = crypto.createSign('SHA256') - expect(verified).toEqual(true) - }) + // Hash challenge using SHA256 + signer.update(challenge) + }) - it('returns false on signature with wrong key - EC Key (secp256k1)', - (): void => { - const realKeyPair = crypto.generateKeyPairSync('ec', { + it('verifies correct signature - EC Key (secp256k1)', (): void => { + const keyPair = crypto.generateKeyPairSync('ec', { namedCurve: 'secp256k1', // Allowed by FIDO spec publicKeyEncoding: { type: 'spki', // Key infrasructure @@ -94,173 +110,203 @@ describe('Signature Verification', (): void => { } }) - const fakeKeyPair = crypto.generateKeyPairSync('ec', { - namedCurve: 'secp256k1', // Allowed by FIDO spec - publicKeyEncoding: { - type: 'spki', // Key infrasructure - format: 'pem' // Encoding format - }, - privateKeyEncoding: { - type: 'pkcs8', - format: 'pem' - } - }) + const signature = signer.sign(keyPair.privateKey, 'base64') + const verified = verifySignature(challenge, signature, keyPair.publicKey) - const signature = signer.sign(fakeKeyPair.privateKey, 'base64') - const verified = verifySignature( - challenge, signature, realKeyPair.publicKey) - - expect(verified).toEqual(false) + expect(verified).toEqual(true) }) - it('returns false for signature based on wrong challenge- EC Key (secp256k1)', - (): void => { - const realKeyPair = crypto.generateKeyPairSync('ec', { - namedCurve: 'secp256k1', // Allowed by FIDO spec - publicKeyEncoding: { - type: 'spki', // Key infrasructure - format: 'pem' // Encoding format - }, - privateKeyEncoding: { - type: 'pkcs8', - format: 'pem' - } - }) + // Using a correct hardcoded key, challenge and signature triplet + it('verifies correct signature - hardcoded EC Key (secp256k1)', (): void => { + const { message, signature, keyPair } = Credential.EC - const fakeKeyPair = crypto.generateKeyPairSync('ec', { - namedCurve: 'secp256k1', // Allowed by FIDO spec - publicKeyEncoding: { - type: 'spki', // Key infrasructure - format: 'pem' // Encoding format - }, - privateKeyEncoding: { - type: 'pkcs8', - format: 'pem' - } - }) + const verified = verifySignature(message, signature, keyPair.public) - // Need another Sign object instead of updating the outer one - // because of parallel test runs - const anotherChallenge = 'This is a different message' - const anotherSigner = crypto.createSign('SHA256') + expect(verified).toEqual(true) + }) - anotherSigner.update(anotherChallenge) + it('returns false on signature with wrong key - EC Key (secp256k1)', + (): void => { + const realKeyPair = crypto.generateKeyPairSync('ec', { + namedCurve: 'secp256k1', // Allowed by FIDO spec + publicKeyEncoding: { + type: 'spki', // Key infrasructure + format: 'pem' // Encoding format + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem' + } + }) - const signature = signer.sign(fakeKeyPair.privateKey, 'base64') - const verified = verifySignature( - challenge, signature, realKeyPair.publicKey) + const fakeKeyPair = crypto.generateKeyPairSync('ec', { + namedCurve: 'secp256k1', // Allowed by FIDO spec + publicKeyEncoding: { + type: 'spki', // Key infrasructure + format: 'pem' // Encoding format + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem' + } + }) - expect(verified).toEqual(false) - }) + const signature = signer.sign(fakeKeyPair.privateKey, 'base64') + const verified = verifySignature( + challenge, signature, realKeyPair.publicKey) - // Using a hardcoded key, challenge and signature triplet - // eslint-disable-next-line max-len - it('returns false for signature based on wrong challenge - hardcoded EC Key (secp256k1)', - (): void => { - const { message, keyPair } = Credential.EC + expect(verified).toEqual(false) + }) - // Need another Sign object instead of updating the outer one - // because of parallel test runs - const anotherChallenge = 'This is a different message' - const anotherSigner = crypto.createSign('SHA256') + it('returns false for signature based on wrong challenge- EC Key (secp256k1)', + (): void => { + const realKeyPair = crypto.generateKeyPairSync('ec', { + namedCurve: 'secp256k1', // Allowed by FIDO spec + publicKeyEncoding: { + type: 'spki', // Key infrasructure + format: 'pem' // Encoding format + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem' + } + }) - anotherSigner.update(anotherChallenge) + const fakeKeyPair = crypto.generateKeyPairSync('ec', { + namedCurve: 'secp256k1', // Allowed by FIDO spec + publicKeyEncoding: { + type: 'spki', // Key infrasructure + format: 'pem' // Encoding format + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'pem' + } + }) - const signature = signer.sign(keyPair.private, 'base64') - const verified = verifySignature(message, signature, keyPair.public) + // Need another Sign object instead of updating the outer one + // because of parallel test runs + const anotherChallenge = 'This is a different message' + const anotherSigner = crypto.createSign('SHA256') - expect(verified).toEqual(false) - }) + anotherSigner.update(anotherChallenge) - it('verifies correct signature - RSA 2048 Key', (): void => { - const realKeyPair = crypto.generateKeyPairSync('rsa', { - modulusLength: 2048 // Key length in bits - }) + const signature = signer.sign(fakeKeyPair.privateKey, 'base64') + const verified = verifySignature( + challenge, signature, realKeyPair.publicKey) - const signature = signer.sign(realKeyPair.privateKey, 'base64') - const verified = verifySignature( - challenge, signature, realKeyPair.publicKey) + expect(verified).toEqual(false) + }) - expect(verified).toEqual(true) - }) + // Using a hardcoded key, challenge and signature triplet + // eslint-disable-next-line max-len + it('returns false for signature based on wrong challenge - hardcoded EC Key (secp256k1)', + (): void => { + const { message, keyPair } = Credential.EC - // Using a correct hardcoded key, challenge and signature triplet - it('verifies correct signature - hardcoded RSA 2048 key', (): void => { - const { message, signature, keyPair } = Credential.RSA + // Need another Sign object instead of updating the outer one + // because of parallel test runs + const anotherChallenge = 'This is a different message' + const anotherSigner = crypto.createSign('SHA256') - const verified = verifySignature(message, signature, keyPair.public) + anotherSigner.update(anotherChallenge) - expect(verified).toEqual(true) - }) + const signature = signer.sign(keyPair.private, 'base64') + const verified = verifySignature(message, signature, keyPair.public) - it('returns false on signature based on wrong key - RSA 2048 Key', - (): void => { - const fakeKeyPair = crypto.generateKeyPairSync('rsa', { - modulusLength: 2048 // Key length in bits + expect(verified).toEqual(false) }) + it('verifies correct signature - RSA 2048 Key', (): void => { const realKeyPair = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 // Key length in bits }) - const signature = signer.sign(fakeKeyPair.privateKey, 'base64') + const signature = signer.sign(realKeyPair.privateKey, 'base64') const verified = verifySignature( challenge, signature, realKeyPair.publicKey) - expect(verified).toEqual(false) + expect(verified).toEqual(true) }) - // Using a hardcoded key, challenge and signature triplet - // eslint-disable-next-line max-len - it('returns false for signature based on wrong challenge - hardcoded RSA 2048 key', - (): void => { - const { message, keyPair } = Credential.RSA - - // Need another Sign object instead of updating the outer one - // because of parallel test runs - const anotherChallenge = 'This is a different message' - const anotherSigner = crypto.createSign('SHA256') - - anotherSigner.update(anotherChallenge) + // Using a correct hardcoded key, challenge and signature triplet + it('verifies correct signature - hardcoded RSA 2048 key', (): void => { + const { message, signature, keyPair } = Credential.RSA - const signature = signer.sign(keyPair.private, 'base64') const verified = verifySignature(message, signature, keyPair.public) - expect(verified).toEqual(false) + expect(verified).toEqual(true) }) - it('properly uses crypto.createVerify function and handles exceptions', - (): void => { - const { message, keyPair } = Credential.RSA + it('returns false on signature based on wrong key - RSA 2048 Key', + (): void => { + const fakeKeyPair = crypto.generateKeyPairSync('rsa', { + modulusLength: 2048 // Key length in bits + }) - // Setting up mocks - const createVerifySpy = jest.spyOn(crypto, 'createVerify') - .mockImplementationOnce((): crypto.Verify => { - throw new Error('Unable to create Verify in mock') + const realKeyPair = crypto.generateKeyPairSync('rsa', { + modulusLength: 2048 // Key length in bits }) - // Setting up the signature - const anotherChallenge = 'This is a different message' - const anotherSigner = crypto.createSign('SHA256') + const signature = signer.sign(fakeKeyPair.privateKey, 'base64') + const verified = verifySignature( + challenge, signature, realKeyPair.publicKey) - anotherSigner.update(anotherChallenge) + expect(verified).toEqual(false) + }) - const signature = signer.sign(keyPair.private, 'base64') + // Using a hardcoded key, challenge and signature triplet + // eslint-disable-next-line max-len + it('returns false for signature based on wrong challenge - hardcoded RSA 2048 key', + (): void => { + const { message, keyPair } = Credential.RSA - // Assertions - expect((): void => { - verifySignature(message, signature, keyPair.public) - }).toThrowError('Unable to create Verify in mock') + // Need another Sign object instead of updating the outer one + // because of parallel test runs + const anotherChallenge = 'This is a different message' + const anotherSigner = crypto.createSign('SHA256') - // Verify that crypto function is called correctly - expect(createVerifySpy).toHaveBeenCalledWith('SHA256') - expect(createVerifySpy).toHaveBeenCalledTimes(1) - // Verify that logger functions are called correctly - // expect(mocked(logger.push)) - // .toHaveBeenCalledWith({ error: new Error('Unable to create Verify in mock') }) - // expect(mocked(logger.push)).toHaveBeenCalledTimes(1) + anotherSigner.update(anotherChallenge) - // expect(mocked(logger.error)).toHaveBeenCalledTimes(1) - // expect(mocked(logger.error)).toHaveBeenCalledWith('Unable to verify signature') - }) + const signature = signer.sign(keyPair.private, 'base64') + const verified = verifySignature(message, signature, keyPair.public) + + expect(verified).toEqual(false) + }) + + it('properly uses crypto.createVerify function and handles exceptions', + (): void => { + const { message, keyPair } = Credential.RSA + + // Setting up mocks + const createVerifySpy = jest.spyOn(crypto, 'createVerify') + .mockImplementationOnce((): crypto.Verify => { + throw new Error('Unable to create Verify in mock') + }) + + // Setting up the signature + const anotherChallenge = 'This is a different message' + const anotherSigner = crypto.createSign('SHA256') + + anotherSigner.update(anotherChallenge) + + const signature = signer.sign(keyPair.private, 'base64') + + // Assertions + expect((): void => { + verifySignature(message, signature, keyPair.public) + }).toThrowError('Unable to create Verify in mock') + + // Verify that crypto function is called correctly + expect(createVerifySpy).toHaveBeenCalledWith('SHA256') + expect(createVerifySpy).toHaveBeenCalledTimes(1) + // Verify that logger functions are called correctly + // expect(mocked(logger.push)) + // .toHaveBeenCalledWith({ error: new Error('Unable to create Verify in mock') }) + // expect(mocked(logger.push)).toHaveBeenCalledTimes(1) + + // expect(mocked(logger.error)).toHaveBeenCalledTimes(1) + // expect(mocked(logger.error)).toHaveBeenCalledWith('Unable to verify signature') + }) + }) }) diff --git a/test/unit/domain/stateMachine/verifyTransaction.model.test.ts b/test/unit/domain/stateMachine/verifyTransaction.model.test.ts index 66b7132c..9263a136 100644 --- a/test/unit/domain/stateMachine/verifyTransaction.model.test.ts +++ b/test/unit/domain/stateMachine/verifyTransaction.model.test.ts @@ -79,6 +79,7 @@ const credential: tpAPI.Schemas.VerifiedCredential = { } } +// yubico example const validConsent: ConsentDomain.Consent = { consentId: 'c121df2a-2a36-4163-ad04-2c8f2913dadd', participantId: 'dfspa', @@ -102,6 +103,8 @@ const validConsent: ConsentDomain.Consent = { createdAt: new Date('2021-01-01'), } + +// yubico example const verificationRequest: tpAPI.Schemas.ThirdpartyRequestsVerificationsPostRequest = { verificationRequestId: '835a8444-8cdc-41ef-bf18-ca4916c2e005', // not a 'real' challenge from mojaloop, but taken from a demo credential here diff --git a/test/unit/shared/fido-lib.test.ts b/test/unit/shared/fido-lib.test.ts index 03e233d9..5305c5b1 100644 --- a/test/unit/shared/fido-lib.test.ts +++ b/test/unit/shared/fido-lib.test.ts @@ -25,10 +25,21 @@ -------------- ******/ import { thirdparty as tpAPI } from '@mojaloop/api-snippets' -import { AttestationResult, ExpectedAttestationResult, Fido2Lib } from 'fido2-lib' +import { AssertionResult, AttestationResult, ExpectedAssertionResult, ExpectedAttestationResult, Fido2Lib } from 'fido2-lib' import str2ab from 'string-to-arraybuffer' import { deriveChallenge } from '~/domain/challenge' import { decodeBase64String } from '~/domain/buffer' +import FidoUtils from '~/shared/fido-utils' + +function ab2str(buf: ArrayBuffer) { + var str = ""; + new Uint8Array(buf).forEach((ch) => { + str += String.fromCharCode(ch); + }); + return str; +} + +const btoa = require('btoa') /* Example attestation result @@ -171,87 +182,20 @@ const consentsPostRequestAUTH = { }, params: {}, payload: { - consentId: '76059a0a-684f-4002-a880-b01159afe119', + consentId: 'be433b9e-9473-4b7d-bdd5-ac5b42463afb', scopes: [ - { - accountId: 'dfspa.username.5678', - actions: [ - 'accounts.transfer' - ] - }, + { actions: ['accounts.getBalance', 'accounts.transfer'], accountId: '412ddd18-07a0-490d-814d-27d0e9af9982' }, + { actions: ['accounts.getBalance'], accountId: '10e88508-e542-4630-be7f-bc0076029ea7' } ], - // todo: make note in api that we are converting all array buffers to base64 encoded strings credential: { credentialType: 'FIDO', - status: 'PENDING', + status: 'VERIFIED', payload: { - id: atob('HskU2gw4np09IUtYNHnxMM696jJHqvccUdBmd0xP6XEWwH0xLei1PUzDJCM19SZ3A2Ex0fNLw0nc2hrIlFnAtw'), - rawId: atob('HskU2gw4np09IUtYNHnxMM696jJHqvccUdBmd0xP6XEWwH0xLei1PUzDJCM19SZ3A2Ex0fNLw0nc2hrIlFnAtw=='), + id: atob('vwWPva1iiTJIk_c7n9a49spEtJZBqrn4SECerci0b-Ue-6Jv9_DZo3rNX02Lq5PU4N5kGlkEPAkIoZ3499AzWQ'), + rawId: atob('vwWPva1iiTJIk/c7n9a49spEtJZBqrn4SECerci0b+Ue+6Jv9/DZo3rNX02Lq5PU4N5kGlkEPAkIoZ3499AzWQ=='), response: { - clientDataJSON: Buffer.from( - [123, 34, 116, 121, - 112, 101, 34, 58, 34, 119, 101, 98, 97, 117, 116, 104, 110, 46, 99, 114, 101, 97, 116, - 101, 34, 44, 34, 99, 104, 97, 108, 108, 101, 110, 103, 101, 34, 58, 34, 89, 122, 82, 104, - 90, 71, 70, 105, 89, 106, 77, 122, 90, 84, 107, 122, 77, 68, 90, 105, 77, 68, 77, 52, 77, - 68, 103, 52, 77, 84, 77, 121, 89, 87, 90, 109, 89, 50, 82, 108, 78, 84, 85, 50, 89, 122, - 85, 119, 90, 68, 103, 121, 90, 106, 89, 119, 77, 50, 89, 48, 78, 122, 99, 120, 77, 87, - 69, 53, 78, 84, 69, 119, 89, 109, 89, 122, 89, 109, 86, 108, 90, 106, 90, 107, 78, 103, - 34, 44, 34, 111, 114, 105, 103, 105, 110, 34, 58, 34, 104, 116, 116, 112, 58, 47, 47, - 108, 111, 99, 97, 108, 104, 111, 115, 116, 58, 52, 50, 49, 56, 49, 34, 44, 34, 99, 114, - 111, 115, 115, 79, 114, 105, 103, 105, 110, 34, 58, 102, 97, 108, 115, 101, 125] - ).toString('base64'), - attestationObject: Buffer.from([163, 99, 102, 109, 116, - 102, 112, 97, 99, 107, 101, 100, 103, 97, 116, 116, 83, 116, 109, 116, 163, 99, 97, 108, - 103, 38, 99, 115, 105, 103, 88, 71, 48, 69, 2, 33, 0, 221, 137, 12, 243, 211, 177, 239, - 248, 228, 65, 210, 169, 42, 68, 38, 40, 168, 147, 155, 39, 179, 225, 234, 116, 151, 33, - 223, 232, 44, 47, 79, 85, 2, 32, 33, 237, 110, 217, 133, 0, 188, 128, 194, 36, 131, 7, 0, - 249, 46, 43, 66, 70, 135, 160, 121, 207, 244, 9, 36, 162, 22, 138, 10, 235, 128, 235, 99, - 120, 53, 99, 129, 89, 2, 193, 48, 130, 2, 189, 48, 130, 1, 165, 160, 3, 2, 1, 2, 2, 4, - 11, 5, 205, 83, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 11, 5, 0, 48, 46, 49, 44, - 48, 42, 6, 3, 85, 4, 3, 19, 35, 89, 117, 98, 105, 99, 111, 32, 85, 50, 70, 32, 82, 111, - 111, 116, 32, 67, 65, 32, 83, 101, 114, 105, 97, 108, 32, 52, 53, 55, 50, 48, 48, 54, 51, - 49, 48, 32, 23, 13, 49, 52, 48, 56, 48, 49, 48, 48, 48, 48, 48, 48, 90, 24, 15, 50, 48, - 53, 48, 48, 57, 48, 52, 48, 48, 48, 48, 48, 48, 90, 48, 110, 49, 11, 48, 9, 6, 3, 85, 4, - 6, 19, 2, 83, 69, 49, 18, 48, 16, 6, 3, 85, 4, 10, 12, 9, 89, 117, 98, 105, 99, 111, 32, - 65, 66, 49, 34, 48, 32, 6, 3, 85, 4, 11, 12, 25, 65, 117, 116, 104, 101, 110, 116, 105, - 99, 97, 116, 111, 114, 32, 65, 116, 116, 101, 115, 116, 97, 116, 105, 111, 110, 49, 39, - 48, 37, 6, 3, 85, 4, 3, 12, 30, 89, 117, 98, 105, 99, 111, 32, 85, 50, 70, 32, 69, 69, - 32, 83, 101, 114, 105, 97, 108, 32, 49, 56, 52, 57, 50, 57, 54, 49, 57, 48, 89, 48, 19, - 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 3, 66, 0, 4, 33, - 26, 111, 177, 181, 137, 37, 203, 10, 193, 24, 95, 124, 42, 227, 168, 180, 136, 16, 20, - 121, 177, 30, 255, 245, 85, 224, 125, 151, 81, 189, 43, 23, 106, 37, 45, 238, 89, 236, - 227, 133, 153, 32, 91, 179, 234, 40, 191, 143, 215, 252, 125, 167, 92, 5, 66, 114, 174, - 72, 88, 229, 145, 252, 90, 163, 108, 48, 106, 48, 34, 6, 9, 43, 6, 1, 4, 1, 130, 196, 10, - 2, 4, 21, 49, 46, 51, 46, 54, 46, 49, 46, 52, 46, 49, 46, 52, 49, 52, 56, 50, 46, 49, 46, - 49, 48, 19, 6, 11, 43, 6, 1, 4, 1, 130, 229, 28, 2, 1, 1, 4, 4, 3, 2, 4, 48, 48, 33, 6, - 11, 43, 6, 1, 4, 1, 130, 229, 28, 1, 1, 4, 4, 18, 4, 16, 20, 154, 32, 33, 142, 246, 65, - 51, 150, 184, 129, 248, 213, 183, 241, 245, 48, 12, 6, 3, 85, 29, 19, 1, 1, 255, 4, 2, - 48, 0, 48, 13, 6, 9, 42, 134, 72, 134, 247, 13, 1, 1, 11, 5, 0, 3, 130, 1, 1, 0, 62, 254, - 163, 223, 61, 42, 224, 114, 87, 143, 126, 4, 208, 221, 90, 75, 104, 219, 1, 175, 232, 99, - 46, 24, 180, 224, 184, 115, 67, 24, 145, 25, 108, 24, 75, 235, 193, 213, 51, 162, 61, - 119, 139, 177, 4, 8, 193, 185, 170, 65, 78, 117, 118, 133, 91, 9, 54, 151, 24, 179, 72, - 175, 92, 239, 108, 176, 48, 134, 114, 214, 31, 184, 189, 155, 134, 161, 10, 166, 130, - 206, 140, 45, 78, 240, 144, 237, 80, 84, 24, 254, 83, 212, 206, 30, 98, 122, 40, 243, - 114, 3, 9, 88, 208, 143, 250, 89, 27, 196, 24, 128, 225, 142, 138, 12, 237, 26, 133, 128, - 127, 144, 150, 113, 65, 122, 11, 69, 50, 21, 179, 141, 193, 71, 42, 36, 73, 118, 64, 180, - 232, 107, 254, 196, 241, 84, 99, 155, 133, 184, 232, 128, 20, 150, 54, 36, 56, 53, 89, 1, - 43, 252, 135, 124, 11, 68, 236, 125, 167, 148, 210, 6, 84, 178, 154, 220, 29, 186, 92, - 80, 123, 240, 202, 109, 243, 82, 188, 205, 222, 116, 13, 46, 167, 225, 8, 36, 162, 206, - 57, 79, 144, 77, 29, 153, 65, 94, 58, 124, 69, 181, 254, 40, 122, 155, 203, 220, 105, - 142, 139, 220, 213, 180, 121, 138, 92, 237, 53, 222, 138, 53, 9, 2, 10, 20, 183, 38, 191, - 191, 57, 167, 68, 7, 156, 185, 143, 91, 157, 202, 9, 183, 195, 235, 188, 189, 162, 175, - 105, 3, 104, 97, 117, 116, 104, 68, 97, 116, 97, 88, 196, 73, 150, 13, 229, 136, 14, 140, - 104, 116, 52, 23, 15, 100, 118, 96, 91, 143, 228, 174, 185, 162, 134, 50, 199, 153, 92, - 243, 186, 131, 29, 151, 99, 65, 0, 0, 0, 4, 20, 154, 32, 33, 142, 246, 65, 51, 150, 184, - 129, 248, 213, 183, 241, 245, 0, 64, 30, 201, 20, 218, 12, 56, 158, 157, 61, 33, 75, 88, - 52, 121, 241, 48, 206, 189, 234, 50, 71, 170, 247, 28, 81, 208, 102, 119, 76, 79, 233, - 113, 22, 192, 125, 49, 45, 232, 181, 61, 76, 195, 36, 35, 53, 245, 38, 119, 3, 97, 49, - 209, 243, 75, 195, 73, 220, 218, 26, 200, 148, 89, 192, 183, 165, 1, 2, 3, 38, 32, 1, 33, - 88, 32, 88, 207, 228, 149, 233, 244, 178, 237, 152, 197, 205, 216, 254, 73, 108, 90, 49, - 183, 218, 195, 134, 83, 251, 6, 32, 10, 83, 119, 191, 221, 228, 85, 34, 88, 32, 100, 179, - 99, 141, 67, 52, 186, 225, 214, 53, 233, 224, 158, 119, 168, 41, 234, 227, 230, 253, 29, - 133, 238, 119, 253, 20, 18, 198, 106, 184, 55, 149] - ).toString('base64') + clientDataJSON: 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiTXpnd056QTFZMkU1TlRaaFlUZzBOMlE0T1dVMFlUUTBOR1JoT1dKbFpXUmpOR1EzTlRZNU1XSTBNV0l3WldNeE9EVTJZalJoWW1Sa05EbGhORE0yTUEiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjQyMTgxIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ==', + attestationObject: 'o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIhAJEFVHrzmq90fdBVy4nOPc48vtvJVAyQleGVcp+nQ8lUAiB67XFnGhC7q7WI3NdcrCdqnewSjCfhqEvO+sbWKC60c2N4NWOBWQLBMIICvTCCAaWgAwIBAgIECwXNUzANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbjELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEnMCUGA1UEAwweWXViaWNvIFUyRiBFRSBTZXJpYWwgMTg0OTI5NjE5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIRpvsbWJJcsKwRhffCrjqLSIEBR5sR7/9VXgfZdRvSsXaiUt7lns44WZIFuz6ii/j9f8fadcBUJyrkhY5ZH8WqNsMGowIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjEwEwYLKwYBBAGC5RwCAQEEBAMCBDAwIQYLKwYBBAGC5RwBAQQEEgQQFJogIY72QTOWuIH41bfx9TAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQA+/qPfPSrgclePfgTQ3VpLaNsBr+hjLhi04LhzQxiRGWwYS+vB1TOiPXeLsQQIwbmqQU51doVbCTaXGLNIr1zvbLAwhnLWH7i9m4ahCqaCzowtTvCQ7VBUGP5T1M4eYnoo83IDCVjQj/pZG8QYgOGOigztGoWAf5CWcUF6C0UyFbONwUcqJEl2QLToa/7E8VRjm4W46IAUljYkODVZASv8h3wLROx9p5TSBlSymtwdulxQe/DKbfNSvM3edA0up+EIJKLOOU+QTR2ZQV46fEW1/ih6m8vcaY6L3NW0eYpc7TXeijUJAgoUtya/vzmnRAecuY9bncoJt8PrvL2ir2kDaGF1dGhEYXRhWMRJlg3liA6MaHQ0Fw9kdmBbj+SuuaKGMseZXPO6gx2XY0EAAAABFJogIY72QTOWuIH41bfx9QBAvwWPva1iiTJIk/c7n9a49spEtJZBqrn4SECerci0b+Ue+6Jv9/DZo3rNX02Lq5PU4N5kGlkEPAkIoZ3499AzWaUBAgMmIAEhWCAITUwire20kCqzl0A3Fbpwx2cnSqwFfTgbA2b8+a/aUiJYIHRMWJlb4Lud02oWTdQ+fejwkVo17qD0KvrwwrZZxWIg', }, type: 'public-key' } @@ -259,11 +203,39 @@ const consentsPostRequestAUTH = { } } +const verificationRequest: tpAPI.Schemas.ThirdpartyRequestsVerificationsPostRequest = { + verificationRequestId: '835a8444-8cdc-41ef-bf18-ca4916c2e005', + // This is stubbed out for pisp-demo-svc + // FIDO library actually signs the base64 hash of this challenge + challenge: btoa('unimplemented123'), + consentId: 'be433b9e-9473-4b7d-bdd5-ac5b42463afb', + signedPayloadType: 'FIDO', + signedPayload: { + id: atob('vwWPva1iiTJIk_c7n9a49spEtJZBqrn4SECerci0b-Ue-6Jv9_DZo3rNX02Lq5PU4N5kGlkEPAkIoZ3499AzWQ'), + rawId: 'vwWPva1iiTJIk_c7n9a49spEtJZBqrn4SECerci0b-Ue-6Jv9_DZo3rNX02Lq5PU4N5kGlkEPAkIoZ3499AzWQ', + response: { + authenticatorData: Buffer.from([73, 150, 13, 229, 136, 14, 140, 104, 116, 52, 23, +15, 100, 118, 96, 91, 143, 228, 174, 185, 162, 134, 50, 199, 153, 92, 243, 186, 131, 29, 151, 99, 1, 0, 0, 0, 18]).toString('base64'), + clientDataJSON: Buffer.from([123, 34, 116, 121, 112, 101, 34, 58, + 34, 119, 101, 98, 97, 117, 116, 104, 110, 46, 103, 101, 116, 34, 44, 34, 99, 104, 97, 108, 108, 101, 110, 103, 101, 34, 58, 34, 100, 87, 53, 112, 98, 88, 66, 115, 90, 87, + 49, 108, 98, 110, 82, 108, 90, 68, 69, 121, 77, 119, 34, 44, 34, 111, 114, 105, 103, 105, 110, 34, 58, 34, 104, 116, 116, 112, 58, 47, 47, 108, 111, 99, 97, 108, 104, + 111, 115, 116, 58, 52, 50, 49, 56, 49, 34, 44, 34, 99, 114, 111, 115, 115, 79, 114, 105, 103, 105, 110, 34, 58, 102, 97, 108, 115, 101, 44, 34, 111, 116, 104, 101, 114, + 95, 107, 101, 121, 115, 95, 99, 97, 110, 95, 98, 101, 95, 97, 100, 100, 101, 100, 95, 104, 101, 114, 101, 34, 58, 34, 100, 111, 32, 110, 111, 116, 32, 99, 111, 109, 112, + 97, 114, 101, 32, 99, 108, 105, 101, 110, 116, 68, 97, 116, 97, 74, 83, 79, 78, 32, 97, 103, 97, 105, 110, 115, 116, 32, 97, 32, 116, 101, 109, 112, 108, 97, 116, 101, + 46, 32, 83, 101, 101, 32, 104, 116, 116, 112, 115, 58, 47, 47, 103, 111, 111, 46, 103, 108, 47, 121, 97, 98, 80, 101, 120, 34, 125]).toString('base64'), + signature: Buffer.from([48, 68, 2, 32, 104, 17, + 39, 167, 189, 118, 136, 100, 84, 72, 120, 29, 255, 74, 131, 59, 254, 132, 36, 19, 184, 24, 93, 103, 67, 195, 25, 252, 6, 224, 120, 69, 2, 32, 56, 251, 234, 96, 138, 6, + 158, 231, 246, 168, 254, 147, 129, 142, 100, 145, 234, 99, 91, 152, 199, 15, 72, 19, 176, 237, 209, 176, 131, 243, 70, 167]).toString('base64') + }, + type: 'public-key' + } +} + // test the fido2-lib for peace of mind describe('fido-lib', (): void => { it('should derive the challenge correctly', () => { // Arrange - const expected = 'YzRhZGFiYjMzZTkzMDZiMDM4MDg4MTMyYWZmY2RlNTU2YzUwZDgyZjYwM2Y0NzcxMWE5NTEwYmYzYmVlZjZkNg==' + const expected = 'MzgwNzA1Y2E5NTZhYTg0N2Q4OWU0YTQ0NGRhOWJlZWRjNGQ3NTY5MWI0MWIwZWMxODU2YjRhYmRkNDlhNDM2MA==' // Act const challenge = deriveChallenge(consentsPostRequestAUTH.payload as tpAPI.Schemas.ConsentsPostRequestAUTH) @@ -276,7 +248,7 @@ describe('fido-lib', (): void => { // Arrange const expected = { "type": "webauthn.create", - "challenge": "YzRhZGFiYjMzZTkzMDZiMDM4MDg4MTMyYWZmY2RlNTU2YzUwZDgyZjYwM2Y0NzcxMWE5NTEwYmYzYmVlZjZkNg", + "challenge": "MzgwNzA1Y2E5NTZhYTg0N2Q4OWU0YTQ0NGRhOWJlZWRjNGQ3NTY5MWI0MWIwZWMxODU2YjRhYmRkNDlhNDM2MA", "origin": "http://localhost:42181", "crossOrigin": false, } @@ -293,14 +265,13 @@ describe('fido-lib', (): void => { it('attestation should succeed', async (): Promise => { // The base challenge that was derived - const challenge = 'YzRhZGFiYjMzZTkzMDZiMDM4MDg4MTMyYWZmY2RlNTU2YzUwZDgyZjYwM2Y0NzcxMWE5NTEwYmYzYmVlZjZkNg==' + const challenge = 'MzgwNzA1Y2E5NTZhYTg0N2Q4OWU0YTQ0NGRhOWJlZWRjNGQ3NTY5MWI0MWIwZWMxODU2YjRhYmRkNDlhNDM2MA==' const attestationExpectations: ExpectedAttestationResult = { challenge, origin: "http://localhost:42181", factor: "either" } - const f2l = new Fido2Lib() const clientAttestationResponse: AttestationResult = { id: str2ab(consentsPostRequestAUTH.payload.credential.payload.id), @@ -311,12 +282,162 @@ describe('fido-lib', (): void => { } } try { - await f2l.attestationResult( + const result = await f2l.attestationResult( clientAttestationResponse, attestationExpectations ) + console.log('credentialPublicKeyPem:', result.authnrData.get('credentialPublicKeyPem')) + + const credIdAB = result.authnrData.get('credId') + const credId = btoa(ab2str(credIdAB)) + console.log('credId:', credId) } catch (error){ throw error } }) + + it('assertion should succeed', async () => { + // Arrange + const f2l = new Fido2Lib() + const assertionExpectations: ExpectedAssertionResult = { + challenge: verificationRequest.challenge, + origin: 'http://localhost:42181', + // fido2lib infers this from origin, so we don't need to set it + // rpId: 'localhost', + factor: "either", + // Get this from the log statement in the previous request + publicKey: `-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECE1MIq3ttJAqs5dANxW6cMdnJ0qs +BX04GwNm/Pmv2lJ0TFiZW+C7ndNqFk3UPn3o8JFaNe6g9Cr68MK2WcViIA== +-----END PUBLIC KEY-----`, + prevCounter: 0, + userHandle: null, + }; + const authenticatorData = FidoUtils.stringToArrayBuffer(verificationRequest.signedPayload.response.authenticatorData) + console.log('authenticatorData.length', authenticatorData.byteLength) + const assertionResult: AssertionResult = { + // fido2lib requires an ArrayBuffer, not just any old Buffer! + id: FidoUtils.stringToArrayBuffer(verificationRequest.signedPayload.id), + response: { + clientDataJSON: verificationRequest.signedPayload.response.clientDataJSON, + authenticatorData, + signature: verificationRequest.signedPayload.response.signature, + userHandle: verificationRequest.signedPayload.response.userHandle + } + } + + // Act + await f2l.assertionResult(assertionResult, assertionExpectations); // will throw on error + + // Assert + }) + + + describe('yubikey site based attestation and assertion', () => { + const credential: tpAPI.Schemas.VerifiedCredential = { + credentialType: 'FIDO', + status: 'VERIFIED', + payload: { + "id": atob("Pdm3TpHQxvmYMdNKcY3R6i8PHRcZqtvSSFssJp0OQawchMOYBnpPQ7E97CPy_caTxPNYVJL-E7cT_HBm4sIuNA"), + "rawId": atob("Pdm3TpHQxvmYMdNKcY3R6i8PHRcZqtvSSFssJp0OQawchMOYBnpPQ7E97CPy_caTxPNYVJL-E7cT_HBm4sIuNA=="), + "response": { + "attestationObject": "o2NmbXRoZmlkby11MmZnYXR0U3RtdKJjc2lnWEgwRgIhAOrrUscl/GRHvjoAtJE6KbgQxUSj3vwp3Ztmh9nQEvuSAiEAgDjZEL8PKFvgJnX7JCk260lOeeht5Ffe/kmA9At17a9jeDVjgVkCwTCCAr0wggGloAMCAQICBAsFzVMwDQYJKoZIhvcNAQELBQAwLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMG4xCzAJBgNVBAYTAlNFMRIwEAYDVQQKDAlZdWJpY28gQUIxIjAgBgNVBAsMGUF1dGhlbnRpY2F0b3IgQXR0ZXN0YXRpb24xJzAlBgNVBAMMHll1YmljbyBVMkYgRUUgU2VyaWFsIDE4NDkyOTYxOTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCEab7G1iSXLCsEYX3wq46i0iBAUebEe//VV4H2XUb0rF2olLe5Z7OOFmSBbs+oov4/X/H2nXAVCcq5IWOWR/FqjbDBqMCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS4xMBMGCysGAQQBguUcAgEBBAQDAgQwMCEGCysGAQQBguUcAQEEBBIEEBSaICGO9kEzlriB+NW38fUwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAPv6j3z0q4HJXj34E0N1aS2jbAa/oYy4YtOC4c0MYkRlsGEvrwdUzoj13i7EECMG5qkFOdXaFWwk2lxizSK9c72ywMIZy1h+4vZuGoQqmgs6MLU7wkO1QVBj+U9TOHmJ6KPNyAwlY0I/6WRvEGIDhjooM7RqFgH+QlnFBegtFMhWzjcFHKiRJdkC06Gv+xPFUY5uFuOiAFJY2JDg1WQEr/Id8C0TsfaeU0gZUsprcHbpcUHvwym3zUrzN3nQNLqfhCCSizjlPkE0dmUFeOnxFtf4oepvL3GmOi9zVtHmKXO013oo1CQIKFLcmv785p0QHnLmPW53KCbfD67y9oq9pA2hhdXRoRGF0YVjExGzvgq0bVGR3WR0Aiwh1nsPm0uy085R0v+ppaZJdA7dBAAAAAAAAAAAAAAAAAAAAAAAAAAAAQD3Zt06R0Mb5mDHTSnGN0eovDx0XGarb0khbLCadDkGsHITDmAZ6T0OxPewj8v3Gk8TzWFSS/hO3E/xwZuLCLjSlAQIDJiABIVggiSfmVgOyesk2SDOaPhShPbnahfrl3Vs0iQUW6QF4IHUiWCDi6beycQU49cvsW32MNlAqXxGJ7uaXY06NOKGq1HraxQ==", + "clientDataJSON": "eyJjaGFsbGVuZ2UiOiJBcEZqVmZSVFF3NV9OUjRZNXBvVHo4a3RkM2dhNGpJNUx5NjJfZzk3b0ZrIiwiY2xpZW50RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoiaHR0cHM6Ly9kZW1vLnl1Ymljby5jb20iLCJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIn0=" + }, + // // front end demo + // id: atob('vwWPva1iiTJIk_c7n9a49spEtJZBqrn4SECerci0b-Ue-6Jv9_DZo3rNX02Lq5PU4N5kGlkEPAkIoZ3499AzWQ'), + // rawId: atob('vwWPva1iiTJIk/c7n9a49spEtJZBqrn4SECerci0b+Ue+6Jv9/DZo3rNX02Lq5PU4N5kGlkEPAkIoZ3499AzWQ=='), + // response: { + // attestationObject: 'o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIhAJEFVHrzmq90fdBVy4nOPc48vtvJVAyQleGVcp+nQ8lUAiB67XFnGhC7q7WI3NdcrCdqnewSjCfhqEvO+sbWKC60c2N4NWOBWQLBMIICvTCCAaWgAwIBAgIECwXNUzANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowbjELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEnMCUGA1UEAwweWXViaWNvIFUyRiBFRSBTZXJpYWwgMTg0OTI5NjE5MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIRpvsbWJJcsKwRhffCrjqLSIEBR5sR7/9VXgfZdRvSsXaiUt7lns44WZIFuz6ii/j9f8fadcBUJyrkhY5ZH8WqNsMGowIgYJKwYBBAGCxAoCBBUxLjMuNi4xLjQuMS40MTQ4Mi4xLjEwEwYLKwYBBAGC5RwCAQEEBAMCBDAwIQYLKwYBBAGC5RwBAQQEEgQQFJogIY72QTOWuIH41bfx9TAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQA+/qPfPSrgclePfgTQ3VpLaNsBr+hjLhi04LhzQxiRGWwYS+vB1TOiPXeLsQQIwbmqQU51doVbCTaXGLNIr1zvbLAwhnLWH7i9m4ahCqaCzowtTvCQ7VBUGP5T1M4eYnoo83IDCVjQj/pZG8QYgOGOigztGoWAf5CWcUF6C0UyFbONwUcqJEl2QLToa/7E8VRjm4W46IAUljYkODVZASv8h3wLROx9p5TSBlSymtwdulxQe/DKbfNSvM3edA0up+EIJKLOOU+QTR2ZQV46fEW1/ih6m8vcaY6L3NW0eYpc7TXeijUJAgoUtya/vzmnRAecuY9bncoJt8PrvL2ir2kDaGF1dGhEYXRhWMRJlg3liA6MaHQ0Fw9kdmBbj+SuuaKGMseZXPO6gx2XY0EAAAABFJogIY72QTOWuIH41bfx9QBAvwWPva1iiTJIk/c7n9a49spEtJZBqrn4SECerci0b+Ue+6Jv9/DZo3rNX02Lq5PU4N5kGlkEPAkIoZ3499AzWaUBAgMmIAEhWCAITUwire20kCqzl0A3Fbpwx2cnSqwFfTgbA2b8+a/aUiJYIHRMWJlb4Lud02oWTdQ+fejwkVo17qD0KvrwwrZZxWIg' + // clientDataJSON: 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiTXpnd056QTFZMkU1TlRaaFlUZzBOMlE0T1dVMFlUUTBOR1JoT1dKbFpXUmpOR1EzTlRZNU1XSTBNV0l3WldNeE9EVTJZalJoWW1Sa05EbGhORE0yTUEiLCJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjQyMTgxIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ==', + // }, + type: 'public-key' + } + } + + const verificationRequest: tpAPI.Schemas.ThirdpartyRequestsVerificationsPostRequest = { + verificationRequestId: '835a8444-8cdc-41ef-bf18-ca4916c2e005', + // not a 'real' challenge from mojaloop, but taken from a demo credential here + // https://demo.yubico.com/webauthn-technical/login + challenge: 'quFYNCTWwfM6VDKmrxTT12zbSOhWJyWglzKoqF0PjMU=', + consentId: '8d34f91d-d078-4077-8263-2c0498dhbjr', + signedPayloadType: 'FIDO', + signedPayload: { + "id": atob("Pdm3TpHQxvmYMdNKcY3R6i8PHRcZqtvSSFssJp0OQawchMOYBnpPQ7E97CPy_caTxPNYVJL-E7cT_HBm4sIuNA"), + "rawId": atob("Pdm3TpHQxvmYMdNKcY3R6i8PHRcZqtvSSFssJp0OQawchMOYBnpPQ7E97CPy_caTxPNYVJL-E7cT_HBm4sIuNA"), + "response": { + "authenticatorData": "xGzvgq0bVGR3WR0Aiwh1nsPm0uy085R0v+ppaZJdA7cBAAAABA==", + "clientDataJSON": "eyJjaGFsbGVuZ2UiOiJxdUZZTkNUV3dmTTZWREttcnhUVDEyemJTT2hXSnlXZ2x6S29xRjBQak1VIiwiY2xpZW50RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoiaHR0cHM6Ly9kZW1vLnl1Ymljby5jb20iLCJ0eXBlIjoid2ViYXV0aG4uZ2V0In0=", + "signature": "MEUCIQCb/nwG57/d8lWXfbBA7HtgIf8wM6A1XJ+LgZlEnClJBAIgKV8FAGkE9B8UXenmp589uTPgkDCJh5jiNMs+Tx2GQG8=" + }, + type: 'public-key' + } + } + + it('performs the attestation', async () => { + // Arrange + + // A random challenge generated by yubikey demo site + const challenge = 'ApFjVfRTQw5_NR4Y5poTz8ktd3ga4jI5Ly62_g97oFk' + const attestationExpectations: ExpectedAttestationResult = { + challenge, + origin: "https://demo.yubico.com", + factor: "either" + } + + const f2l = new Fido2Lib() + const clientAttestationResponse: AttestationResult = { + id: str2ab(credential.payload.id), + rawId: str2ab(credential.payload.rawId), + response: { + clientDataJSON: credential.payload.response.clientDataJSON, + attestationObject: credential.payload.response.attestationObject, + } + } + + // Act + const result = await f2l.attestationResult( + clientAttestationResponse, + attestationExpectations + ) + console.log('credentialPublicKeyPem:', result.authnrData.get('credentialPublicKeyPem')) + + // Assert + // nothing threw! + }) + + it('performs the assertion', async () => { + // Arrange + const f2l = new Fido2Lib() + const assertionExpectations: ExpectedAssertionResult = { + challenge: verificationRequest.challenge, + origin: 'https://demo.yubico.com', + factor: "either", + // Get this from the log statement in the previous request + publicKey: `-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiSfmVgOyesk2SDOaPhShPbnahfrl +3Vs0iQUW6QF4IHXi6beycQU49cvsW32MNlAqXxGJ7uaXY06NOKGq1HraxQ== +-----END PUBLIC KEY-----`, + prevCounter: 0, + userHandle: null, + }; + const authenticatorData = FidoUtils.stringToArrayBuffer(verificationRequest.signedPayload.response.authenticatorData) + console.log('authenticatorData.length', authenticatorData.byteLength) + const assertionResult: AssertionResult = { + // fido2lib requires an ArrayBuffer, not just any old Buffer! + id: FidoUtils.stringToArrayBuffer(verificationRequest.signedPayload.id), + response: { + clientDataJSON: verificationRequest.signedPayload.response.clientDataJSON, + authenticatorData, + signature: verificationRequest.signedPayload.response.signature, + userHandle: verificationRequest.signedPayload.response.userHandle + } + } + + // Act + await f2l.assertionResult(assertionResult, assertionExpectations); // will throw on error + + // Assert + }) + }) })