diff --git a/app/sample/CreateTransferTokenWithUnusualOptionsSample.js b/app/sample/CreateTransferTokenWithUnusualOptionsSample.js index 389b6542..114ebce7 100644 --- a/app/sample/CreateTransferTokenWithUnusualOptionsSample.js +++ b/app/sample/CreateTransferTokenWithUnusualOptionsSample.js @@ -19,7 +19,6 @@ export default async (payer, payee) => { .setRefId('a713c8a61994a749') .setChargeAmount(10.0) .setDescription('Book purchase') - .setPurposeOfPayment('PERSONAL_EXPENSES') .execute(); // Payer endorses the token, creating a digital signature on it diff --git a/core/src/http/AuthHttpClient.js b/core/src/http/AuthHttpClient.js index afe1f934..353bd6e5 100644 --- a/core/src/http/AuthHttpClient.js +++ b/core/src/http/AuthHttpClient.js @@ -51,32 +51,32 @@ export class AuthHttpClient { /** * Creates the necessary signer objects, based on the level requested. - * If the level is not available, attempts to fetch a lower level. + * If the level is not available, attempts to fetch a higher level. * * @param {string} level - requested level of key * @return {Promise} object used to sign */ async getSigner(level) { if (level === config.KeyLevel.LOW) { - return await this._cryptoEngine.createSigner(config.KeyLevel.LOW); + try { + return await this._cryptoEngine.createSigner(config.KeyLevel.LOW); + } catch (err) { + try { + return await this._cryptoEngine.createSigner(config.KeyLevel.STANDARD); + } catch (err) { + return await this._cryptoEngine.createSigner(config.KeyLevel.PRIVILEGED); + } + } } if (level === config.KeyLevel.STANDARD) { try { return await this._cryptoEngine.createSigner(config.KeyLevel.STANDARD); } catch (err) { - return await this._cryptoEngine.createSigner(config.KeyLevel.LOW); + return await this._cryptoEngine.createSigner(config.KeyLevel.PRIVILEGED); } } if (level === config.KeyLevel.PRIVILEGED) { - try { - return await this._cryptoEngine.createSigner(config.KeyLevel.PRIVILEGED); - } catch (err) { - try { - return await this._cryptoEngine.createSigner(config.KeyLevel.STANDARD); - } catch (err2) { - return await this._cryptoEngine.createSigner(config.KeyLevel.LOW); - } - } + return await this._cryptoEngine.createSigner(config.KeyLevel.PRIVILEGED); } } diff --git a/core/src/http/HttpClient.js b/core/src/http/HttpClient.js index 175c78d6..3612db5e 100644 --- a/core/src/http/HttpClient.js +++ b/core/src/http/HttpClient.js @@ -79,6 +79,51 @@ export class HttpClient { return this._instance(request); } + /** + * Update member + * + * @param memberId - ID of the member + * @param {string} prevHash - member's last hash + * @param {Array} keys - keys to add + * @param {Object} signer - signer for keyId and signature(updating signature) + * @param {Array} recover - recovery rules for member + * @returns {Object} response to the API call + */ + async updateMember(memberId, prevHash, keys, signer, recover){ + const operation = + keys.map(key => ({ + addKey: { + key: { + id: key.id, + publicKey: Util.strKey(key.publicKey), + level: key.level, + algorithm: key.algorithm, + ...key.expiresAtMs && {expiresAtMs: key.expiresAtMs}, + }, + }, + })); + recover.forEach(value => operation.push({recover: value})); + const update = { + memberId: memberId, + prevHash: prevHash, + operations: operation, + }; + const req = { + update, + updateSignature: { + memberId: memberId, + keyId: signer.getKeyId(), + signature: await signer.signJson(update), + }, + }; + const request = { + method: 'post', + url: `/members/${memberId}/updates`, + data: req, + }; + return this._instance(request); + } + /** * Gets banks or countries. * diff --git a/core/src/security/Crypto.js b/core/src/security/Crypto.js index 06a07918..9dce31af 100644 --- a/core/src/security/Crypto.js +++ b/core/src/security/Crypto.js @@ -3,6 +3,7 @@ import {base64Url} from './Base64UrlCodec'; import sha256 from 'fast-sha256'; import nacl from 'tweetnacl'; import Util from '../Util'; +import forge from 'node-forge'; /** * Class providing static crypto primitives. @@ -27,6 +28,26 @@ class Crypto { return keyPair; } + /** + * + * @param keyPair - rsa key pair + * @param {string} keyId - key id + * @param {string} keyLevel - 'LOW', 'STANDARD', or 'PRIVILEGED' + * @param {number} expirationMs - (optional) expiration duration of the key in milliseconds + * @return {Object} formatted rsa key pair + */ + static generateRsaKeys(keyPair, keyId, keyLevel, expirationMs) { + const keys = {}; + keys.publicKey = keyPair.publicKey; + keys.id = keyId; + keys.algorithm = 'RSA'; + keys.level = keyLevel; + keys.privateKey = keyPair.privateKey; + if (expirationMs) + keys.expiresAtMs = ((new Date()).getTime() + expirationMs).toString(); + return keys; + } + /** * Signs a json object and returns the signature * @@ -46,6 +67,12 @@ class Crypto { * @return {string} signature */ static sign(message, keys) { + if(keys.algorithm === 'RSA') { + const md = forge.md.sha256.create(); + md.update(message, 'utf8'); + return forge.util.encode64(keys.privateKey.sign(md)) + .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); + } const msg = Util.wrapBuffer(message); return base64Url(nacl.sign.detached(msg, keys.privateKey)); } diff --git a/core/src/security/engines/KeyStoreCryptoEngine.js b/core/src/security/engines/KeyStoreCryptoEngine.js index bea71dec..209bfdaa 100644 --- a/core/src/security/engines/KeyStoreCryptoEngine.js +++ b/core/src/security/engines/KeyStoreCryptoEngine.js @@ -47,6 +47,23 @@ export class KeyStoreCryptoEngine { return stored; } + /** + * Encapsulate rsa key pair data + * + * @param keyPair rsa key pair. + * @param keyId - key Id. + * @param keyLevel - 'LOW', 'STANDARD', or 'PRIVILEGED' + * @param expirationMs - (optional) expiration duration of the key in milliseconds + * @return formatted rsa key data. + */ + async generateRsaKey( + keyPair: Object, keyId: string, keyLevel, expirationMs?: number | string + ): Object { + const keys = await this._crypto.generateRsaKeys(keyPair, keyId, keyLevel, expirationMs); + const stored = await this._keystore.put(this._memberId, keys); + return stored; + } + /** * Creates a signer. Assumes we previously generated the relevant key. * @@ -58,6 +75,17 @@ export class KeyStoreCryptoEngine { return this._crypto.createSignerFromKeyPair(keyPair); } + /** + * Creates a new signer using a key with a specified id. + * + * @param keyId key Id + * @returns signer object that implements sign, signJson, and getKeyId + */ + async createSignerById(keyId: string): Object { + const keyPair = await this._keystore.getById(this._memberId, keyId); + return this._crypto.createSignerFromKeyPair(keyPair); + } + /** * Creates a verifier. Assumes we have the key with the passed ID. * diff --git a/core/src/types.js b/core/src/types.js index 6a95b0fd..e74b88a4 100644 --- a/core/src/types.js +++ b/core/src/types.js @@ -8,6 +8,10 @@ export type VerificationStatus = 'INVALID' | 'FAILURE_ERROR_RESPONSE' // verification service returned an error response | 'FAILURE_ERROR' // an error happened during the verification process | 'IN_PROGRESS'; // certificate validation has not finished yet, use getEidasVerificationStatus call to get the result later +export type EidasCertificateStatus = 'INVALID_CERTIFICATE_STATUS' + | 'CERTIFICATE_VALID' + | 'CERTIFICATE_INVALID' + | 'CERTIFICATE_NOT_FOUND'; export type NotificationStatus = 'PENDING' | 'DELIVERED' | 'COMPLETED' | 'INVALIDATED'; export type NotifyStatus = 'ACCEPTED' | 'NO_SUBSCRIBERS'; export type TokenOperationStatus = 'SUCCESS' | 'MORE_SIGNATURES_NEEDED'; @@ -49,6 +53,17 @@ export type EventType = 'INVALID' | 'TRANSFER_STATUS_CHANGED' | 'BULK_TRANSFER_STATUS_CHANGED'; +export type OpenBankingStandard = + 'Invalid_Standard' + | 'UK_Open_Banking_Standard' + | 'Starling_Bank_API' + | 'PolishAPI' + | 'STET_PSD2_API' + | 'Citi_Handlowy_PSD2_API' + | 'NextGenPSD2' + | 'Slovak_Banking_API_Standard' + | 'Czech_Open_Banking_Standard'; + export type Alias = { type: AliasType, value: string, @@ -360,7 +375,17 @@ export type GetEidasVerificationStatusResponse = { statusDetails: string, }; +export type GetEidasCertificateStatusResponse = { + status: EidasCertificateStatus, + certificate: string, +}; + export type WebhookConfig = { url: string, type: Array, }; + +export type RegisterWithEidasPayload = { + bankId: string, + certificate: string, +}; \ No newline at end of file diff --git a/tpp/sample/VerifyEidasSample.js b/tpp/sample/VerifyEidasSample.js index 15d193da..461d60e0 100644 --- a/tpp/sample/VerifyEidasSample.js +++ b/tpp/sample/VerifyEidasSample.js @@ -1,6 +1,9 @@ import {TokenClient} from '../src'; import stringify from 'fast-json-stable-stringify'; import forge from 'node-forge'; +import KeyStoreCryptoEngine from '../../core/src/security/engines/KeyStoreCryptoEngine'; +import MemoryKeyStore from '../../core/src/security/engines/MemoryKeyStore'; +const {ENV: TEST_ENV = 'dev'} = process.env; class VerifyEidasSample { /** @@ -53,6 +56,140 @@ class VerifyEidasSample { const verificationStatus = await tpp.getEidasVerificationStatus(response.verificationId); return tpp; } + + /** + * Recovers a TPP member and verifies its EIDAS alias using eIDAS certificate. + * + * @param memberId id of the member to be recovered + * @param tppAuthNumber authNumber of the TPP + * @param certificate base64 encoded eIDAS certificate (a single line, no header and footer) + * @param privateKey private key corresponding to the public key in the certificate + * @returns verified business member. + */ + static async recoverEidas(memberId, tppAuthNumber, certificate, privateKey) { + const devKey = require('../src/config.json').devKey[TEST_ENV]; + const Token = new TokenClient({env: TEST_ENV, developerKey: devKey}); + const engine = new KeyStoreCryptoEngine(memberId, new MemoryKeyStore()); + const key = await engine.generateKey('PRIVILEGED'); + const payload = { + memberId: memberId, + certificate: certificate, + algorithm: 'RS256', + key: key, + }; + const md = forge.md.sha256.create(); + md.update(stringify(payload), 'utf8'); + const signature = forge.util.encode64(privateKey.sign(md)) + .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); + const recoveredMember = await Token.recoverEidasMember( + payload, signature, engine); + const eidasAlias = { + type: 'EIDAS', + value: tppAuthNumber, + realmId: recoveredMember._options.realmId, + }; + const verifyPayload = { + memberId: memberId, + alias: eidasAlias, + certificate: certificate, + algorithm: 'RS256', + }; + const md1 = forge.md.sha256.create(); + md1.update(stringify(verifyPayload), 'utf8'); + const signature1 = forge.util.encode64(privateKey.sign(md1)) + .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); + await recoveredMember.verifyEidas(verifyPayload, signature1); + return recoveredMember; + } + + /** + * Creates a TPP member under realm of a bank and registers it with the provided eIDAS + * certificate. The created member has a registered PRIVILEGED-level RSA key from the provided + * certificate and an EIDAS alias with value equal to authNumber from the certificate. + * + * @param keyPair eIDAS key pair for the provided certificate + * @param keyStore + * @param bankId id of the bank the TPP trying to get access to + * @param certificate base64 encoded eIDAS certificate (a single line, no header and footer) + * @returns a newly created Member. + */ + static async registerWithEidas(keyPair, keyStore, bankId, certificate) { + const devKey = require('../src/config.json').devKey[TEST_ENV]; + const Token = new TokenClient({env: TEST_ENV, developerKey: devKey}); + const payload = { + certificate: certificate, + bankId: bankId, + }; + // sign payload with the provided private key + const md = forge.md.sha256.create(); + md.update(stringify(payload), 'utf8'); + const signature = forge.util.encode64(keyPair.privateKey.sign(md)) + .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); + const resp = await Token.registerWithEidas(payload, signature); + keyStore.put(resp.memberId, + await Token.Crypto.generateRsaKeys(keyPair, resp.keyId,'PRIVILEGED')); + const member = await Token.getMember(Token.MemoryCryptoEngine, resp.memberId); + const verificationStatus = await member.getEidasVerificationStatus( + resp.verificationId); + return member; + } + + static generateCert(keys, authNumber) { + const cert = forge.pki.createCertificate(); + cert.publicKey = keys.publicKey; + cert.serialNumber = `${Date.now()}`; + cert.validity.notBefore = new Date(); + cert.validity.notAfter = new Date(); + cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1); + const attrs = [{ + name: 'commonName', + value: 'Token.io', + }, { + name: 'countryName', + value: 'UK', + }, { + name: 'id_at_organizationIdentifier', + type: '2.5.4.97', + value: authNumber, + }]; + cert.setSubject(attrs); + cert.setIssuer(attrs); + cert.setExtensions([{ + name: 'basicConstraints', + cA: true, + }, { + name: 'keyUsage', + keyCertSign: true, + digitalSignature: true, + nonRepudiation: true, + keyEncipherment: true, + dataEncipherment: true, + }, { + name: 'extKeyUsage', + serverAuth: true, + clientAuth: true, + codeSigning: true, + emailProtection: true, + timeStamping: true, + }, { + name: 'nsCertType', + client: true, + server: true, + email: true, + objsign: true, + sslCA: true, + emailCA: true, + objCA: true, + }]); + + // self-sign certificate + cert.sign(keys.privateKey, forge.md.sha256.create()); + + // PEM-format cert + const certPem = forge.pki.certificateToPem(cert); + const certDer = forge.pki.pemToDer(certPem); + return forge.util.encode64(certDer.data); + } } export default VerifyEidasSample; diff --git a/tpp/src/http/AuthHttpClient.js b/tpp/src/http/AuthHttpClient.js index 3aff60b1..30826fff 100644 --- a/tpp/src/http/AuthHttpClient.js +++ b/tpp/src/http/AuthHttpClient.js @@ -387,6 +387,19 @@ class AuthHttpClient extends CoreAuthHttpClient { return this._instance(request); } + /** + * Get status of the current eIDAS certificate along with the certificate itself. + * + * @return eidas status and the eidas certificate, if any + */ + async getEidasCertificateStatus() { + const request = { + method: 'get', + url: '/eidas/status', + }; + return this._instance(request); + } + /** * Redeem a bulk transfer token, creating a bulk transfer. * @@ -450,15 +463,15 @@ class AuthHttpClient extends CoreAuthHttpClient { } /** - * Get the raw consent from the bank associated with a token. + * Get the external metadata from the bank associated with a token request. * - * @param tokenId {string} Token Id - * @returns {Object} response to the api call + * @param tokenRequestId token request ID + * @return external metadata */ - async getRawConsent(tokenId){ + async getExternalMetadata(tokenRequestId){ const request = { method: 'get', - url: `/tokens/${tokenId}/consent`, + url: `/token-requests/${tokenRequestId}/external-metadata`, }; return this._instance(request); } diff --git a/tpp/src/http/HttpClient.js b/tpp/src/http/HttpClient.js index 9475852e..f61fad5a 100644 --- a/tpp/src/http/HttpClient.js +++ b/tpp/src/http/HttpClient.js @@ -22,6 +22,48 @@ class HttpClient extends CoreHttpClient{ return this._instance(request); } + /** + * Create and onboard a business member under realm of a bank using eIDAS certificate. + * + * @param payload payload with eIDAS certificate and bank id + * @param signature payload signed with the private key corresponding to the certificate + * public key + * @return {Promise} response to the API call + */ + async registerWithEidas(payload, signature) { + const req = { + payload, + signature, + }; + const request = { + method: 'put', + url: '/eidas/register', + data: req, + }; + return this._instance(request); + } + + /** + * Recovers an eIDAS-verified member with eidas payload. + * + * @param payload a payload containing member id, the certificate and a new key to add to the + * member + * @param signature a payload signature with the private key corresponding to the certificate + * @return {Promise} response to the API call + */ + async recoverEidasMember(payload, signature) { + const req = { + payload, + signature, + }; + const request = { + method: 'post', + url: '/eidas/recovery/operations', + data: req, + }; + return this._instance(request); + } + /** * Get the token request result based on its token request ID. * diff --git a/tpp/src/main/ExternalMetadata.js b/tpp/src/main/ExternalMetadata.js new file mode 100644 index 00000000..7326700e --- /dev/null +++ b/tpp/src/main/ExternalMetadata.js @@ -0,0 +1,28 @@ +// @flow +import type {OpenBankingStandard} from '@token-io/core'; + +class ExternalMetadata{ + openBankingStandard: OpenBankingStandard; + consent: string; + + /** + * Instantiates a new external metadata instance. + * + * @param openBankingStandard the open banking standard + * @param consent the consent + */ + constructor(openBankingStandard: OpenBankingStandard, consent: string) { + this.openBankingStandard = openBankingStandard; + this.consent = consent; + } + + getOpenBankingStandard(): OpenBankingStandard{ + return this.openBankingStandard; + } + + getConsent(): string{ + return this.consent; + } +} + +export default ExternalMetadata; diff --git a/tpp/src/main/Member.js b/tpp/src/main/Member.js index 93e0376c..12bd8b77 100644 --- a/tpp/src/main/Member.js +++ b/tpp/src/main/Member.js @@ -2,6 +2,7 @@ import {Member as CoreMember, Account} from '@token-io/core'; import config from '../config.json'; import Representable from './Representable'; +import ExternalMetadata from './ExternalMetadata'; import TokenRequestBuilder from './TokenRequestBuilder'; import AuthHttpClient from '../http/AuthHttpClient'; import HttpClient from '../http/HttpClient'; @@ -23,6 +24,7 @@ import type { VerifyEidasResponse, CustomerTrackingMetadata, GetEidasVerificationStatusResponse, + GetEidasCertificateStatusResponse, WebhookConfig, } from '@token-io/core'; @@ -132,6 +134,19 @@ export default class Member extends CoreMember { }); } + /** + * Get the external metadata from the bank associated with a token request. + * + * @param tokenRequestId token request ID + * @return external metadata + */ + getExternalMetadata(tokenRequestId: string): Promise{ + return Util.callAsync(this.getExternalMetadata,async () => { + const res = await this._client.getExternalMetadata(tokenRequestId); + return new ExternalMetadata(res.data.standard, res.data.consent); + }); + } + /** * Uploads the authenticated member's public profile. * @@ -527,6 +542,18 @@ export default class Member extends CoreMember { }); } + /** + * Get status of the current eIDAS certificate along with the certificate itself, if present. + * + * @return eidas status and the eidas certificate, if any + */ + async getEidasCertificateStatus(): Promise { + return Util.callAsync(this.getEidasCertificateStatus, async () => { + const res = await this._client.getEidasCertificateStatus(); + return res.data; + }); + } + /** * Get url to bank authorization page for a token request. * @@ -556,19 +583,6 @@ export default class Member extends CoreMember { }); } - /** - * Get the raw consent from the bank associated with a token. - * - * @param tokenId {string} Token Id - * @returns {string} raw consent - */ - getRawConsent(tokenId: string): Promise { - return Util.callAsync(this.getRawConsent, async () => { - const res = await this._client.getRawConsent(tokenId); - return res.data.consent; - }); - } - /** * Set a webhook config. The config contains a url and a list of event types. * diff --git a/tpp/src/main/TokenClient.js b/tpp/src/main/TokenClient.js index 6b887614..f8e95b72 100644 --- a/tpp/src/main/TokenClient.js +++ b/tpp/src/main/TokenClient.js @@ -18,6 +18,7 @@ import type { TokenRequestTransferDestinationsCallbackParameters, TransferEndpoint, BulkTransferBodyTransfers, + RegisterWithEidasPayload, } from '@token-io/core'; import BulkTransferTokenRequestBuilder from './BulkTransferTokenRequestBuilder'; @@ -322,6 +323,63 @@ export class TokenClient extends Core { }); } + /** + * Recovers an eIDAS-verified member with eIDAS payload. + * + * @param payload a payload containing member id, the certificate and a new key to add to the + * member + * @param signature a payload signature with the private key corresponding to the certificate + * @param engine a crypto engine that must contain the privileged key that is included in + * the payload (if it does not contain keys for other levels they will be generated) + * @return a new member + */ + recoverEidasMember( + payload: EidasRecoveryPayload, + signature: string, + engine: KeyStoreCryptoEngine + ): Promise { + const memberId = payload.memberId; + const devKey = require('../../src/config.json').devKey[TEST_ENV]; + return Util.callAsync(this.recoverEidasMember, async () => { + const privilegedKey = payload.key; + const standardKey = await engine.generateKey('STANDARD'); + const lowKey = await engine.generateKey('LOW'); + const keys = [privilegedKey, standardKey, lowKey]; + const signer = await engine.createSignerById(privilegedKey.id); + const response = await this._unauthenticatedClient.recoverEidasMember( + payload, signature); + const memberRes = await this._unauthenticatedClient.getMember(memberId); + const updateRes = await this._unauthenticatedClient.updateMember( + memberId, memberRes.data.member.lastHash, keys, signer, + [response.data.recoveryEntry]); + return new Member( + {env: TEST_ENV, memberId: updateRes.data.member.id, + realmId: updateRes.data.member.realmId, cryptoEngine: engine, + developerKey: devKey}); + }); + } + + /** + * Creates a business member under realm of a bank with an EIDAS alias (with value equal to the + * authNumber from the certificate) and a PRIVILEGED-level public key taken from the + * certificate. Then onboards the member with the provided certificate. + * A successful onboarding includes verifying the member and the alias and adding permissions + * based on the certificate. + * + * @param payload payload with eidas certificate and bank id + * @param signature payload signed with private key corresponding to the certificate public key + * @returns member id, key id and id of the certificate verification request + */ + registerWithEidas( + payload: RegisterWithEidasPayload, + signature: string + ): Promise<{memberId: memberId, keyId: keyId, verificationId: verificationId}> { + return Util.callAsync(this.registerWithEidas, async () => { + const res = await this._unauthenticatedClient.registerWithEidas(payload, signature); + return res.data; + }); + } + /** * Gets the token request result based on its token request ID. * diff --git a/tpp/test/sample/VerifyEidasSample.spec.js b/tpp/test/sample/VerifyEidasSample.spec.js index 39423f1d..77f37c44 100755 --- a/tpp/test/sample/VerifyEidasSample.spec.js +++ b/tpp/test/sample/VerifyEidasSample.spec.js @@ -1,62 +1,56 @@ import VerifyEidasSample from '../../sample/VerifyEidasSample'; import forge from 'node-forge'; const {assert} = require('chai'); +import Util from '../../src/Util'; +import MemoryKeyStore from '../../../core/src/security/engines/MemoryKeyStore'; describe('VerifyEidasSample test', () => { it('Should create and verify business member', async () => { - // input: TPP authNumber, eIDAS certificate and private key corresponding - // to the public key of the certificate - const tppAuthNumber = 'testAuthNumber'; - const privateKey = forge.pki.privateKeyFromPem( - '-----BEGIN RSA PRIVATE KEY-----\n' + - 'MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDNnoOVE+kXQ8Fb\n' + - '0W9XbwSdpWuydefP/qKxexD7s/ifX+zhfVyEPAlw87fok0Vvu+oBwaSv9xBMyHO6\n' + - 'DQWVVJKFmCr5k/RBFQLcHlwxUtuffzBI0bXSEi9yEhJNfLxh1C6JKOgNcY8RvBBo\n' + - 'QYony21kwiE1u/2f4uOgrJSpbaXcyRF93QamJZPnErLVH3Ift2srXLhTECQlQeqR\n' + - 'XfozvCXiT8CE99BX+dLnP1DPLe0ZJuJnoy+CZdVUZlXxquqThG9OnITx8ulKesuC\n' + - 'cOl3lKcOKTL5MSXjzUuwxHV6J/LnUOXA+UIkbwWnbx9n7CuUGdT5Dl8lV6hM0s/T\n' + - 'USO+uBXDAgMBAAECggEAYvQjp/Vmiisx3VG7zyye4I1Q+JgUgP0u59HtmOyCPJTq\n' + - 'B5SQlxTbiE/KFHP3iS+6jLDX5FU2s3UOeJ22r6h9QSy7ldO1yzJk53D29kfFaQtJ\n' + - 'PeoqodfdnHyE8WSTtlLqOFkG4B1j+Gl0ze+ooMEeOszQNa9sfesvl84mVyltFoSS\n' + - 'e38pCTLKk2N/CGeCbO436Pt1d6weWf9U8pPg0n1WQ8reTKQXVa91Fbqnpbydtyd+\n' + - 'ycdB+T1rOCl6dyRm9mkz0pBSQGza7TmTDoxPfiHyUtOK/omokNeg/5y8uMRycl2A\n' + - 'a/aFiBFzI+cn30kuVo+AazDRHU6UgSmjRcfjpijjrQKBgQDo1UDB5LTKzB+SD+J/\n' + - 'FDcjhxdqbsX+5j3giKKxlVViiOaLHpO1unz6adJ3ks9V8BkUMZebwvPTi6VDaJN/\n' + - 'qJBhfWTJUZc2DF7iYkEBvBxoRVYVtJC2wCRFTBb1FtRxTYQb8rZ4no8/n3UQaGHD\n' + - 'U0o0xPGwen8MdGM6rS+t9EiYrQKBgQDiFBFSHfYdMq7DzSbQ9XmXyAWyg8WhXQSf\n' + - 'wefevoSxFdyg+uON40ge6fUFa6fm35IeN1waF9m0HBmZ84Yv1UBweIc8Bi7DPobr\n' + - 'mrtPnqjbbru3DGVyzt7aVVKo2ajksC62D9ZHGOoNQuohlQTyFjUMrMyFtX5pjDW0\n' + - 'zRR1IxAGLwKBgQCYP0n2/3wQ1+UU93odqrRbcOyo1gJ2KUvw1Ke+A24v3mESO05P\n' + - 'GMC8ZhIPyln1Uei0QbFnUtVpUPkh2PIKGck/VmVfFxEPcIti8OidH8pbjGs8CjRO\n' + - 'q9mLzrN8VA9af1uRguY6fUQiUDyWHAtBU+dEFjwMMC1/kWOJbNGup/wIiQKBgQCw\n' + - 'Tb4EL+FSe8fWYhI3OneMaiwnPqPMZuHIREsyZZjNEKNx1rXGXMxNb13o0D+ryAYH\n' + - 'Elz87ESWNKOybzrh6ofKLfQoVxn4oLZO3efc+3nrRbuV0FD8617XHVrM2pDfZpXG\n' + - '3SrZXxCHLvuvHKsyrybHr6n/S749SV5IlPWzM5i3eQKBgCikhJUZIQNC+uXQwix3\n' + - 'AUENA314gZLqgotH44GN+1nsZ0GzKoUirj/1Lz1ag/gFom/nsfs16cPn5EjF6Yxb\n' + - 'OaQWvfXF6zuxkpECeXIdcB1jVVo7NUaof4b+2YSIUhlwU9nxGHqSUhCImNXIO7dj\n' + - 'zkugF61BGMIO6QgEfJLs6QoS\n' + - '-----END RSA PRIVATE KEY-----'); - const certificate = 'MIIC6zCCAdOgAwIBAgIGAW2OBKSAMA0GCSqGSIb3DQEBCwUAMCwxETAPBgNVBAMMCF' + - 'Rva2VuLmlvMRcwFQYDVQRhDA50ZXN0QXV0aE51bWJlcjAeFw0xOTEwMDIxOTQ4NTFaFw0yMDEwMDIxOTQ4' + - 'NTFaMCwxETAPBgNVBAMMCFRva2VuLmlvMRcwFQYDVQRhDA50ZXN0QXV0aE51bWJlcjCCASIwDQYJKoZIhv' + - 'cNAQEBBQADggEPADCCAQoCggEBAM2eg5UT6RdDwVvRb1dvBJ2la7J158/+orF7EPuz+J9f7OF9XIQ8CXDzt' + - '+iTRW+76gHBpK/3EEzIc7oNBZVUkoWYKvmT9EEVAtweXDFS259/MEjRtdISL3ISEk18vGHULoko6A1xjxG8' + - 'EGhBiifLbWTCITW7/Z/i46CslKltpdzJEX3dBqYlk+cSstUfch+3aytcuFMQJCVB6pFd+jO8JeJPwIT30Ff' + - '50uc/UM8t7Rkm4mejL4Jl1VRmVfGq6pOEb06chPHy6Up6y4Jw6XeUpw4pMvkxJePNS7DEdXon8udQ5cD5Qi' + - 'RvBadvH2fsK5QZ1PkOXyVXqEzSz9NRI764FcMCAwEAAaMTMBEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9' + - 'w0BAQsFAAOCAQEACzdex+RGLj+7YDwiYbJk40SpzDlk4ns2Bk5U/aMKPbsVsxu0q4w8/envM8TB8Z8IrMW5' + - 'axHv2rXaQ59TQvzMEx3NXSCzeS7ylOeNFokIgjbDVojVTpHwwXq74GavX1J1aMk0sopwmb1wF8WvpmbAH7z' + - 'EByNpQn6qfftZ3iM9qVFE/o+sx9CbQ1KAHJUHOPjFMkwHDcgXByyW7j5+TCAHpk281s3GfziUMeM7BoOFrK' + - '+5F8ergolcQfOsruNfVHYWhCHvMT4ICFydSJnoMTKT7g41KA3HWEUr0hqNLHTaypahUFGEVroRxMS5HlTGb' + - 'DBl2qEf37t3zh636m0ndS5x9A=='; - + const keyPair = forge.pki.rsa.generateKeyPair(); + const tppAuthNumber = Util.generateNonce(); + const certificate = VerifyEidasSample.generateCert(keyPair, tppAuthNumber); const member = await VerifyEidasSample.verifyEidas( - certificate, tppAuthNumber, 'gold', privateKey); + certificate, tppAuthNumber, 'gold', keyPair.privateKey); const aliases = await member.aliases(); assert.equal(aliases.length, 1); assert.equal(aliases[0].value, tppAuthNumber); assert.equal(aliases[0].type, 'EIDAS'); + const eidasInfo = await member.getEidasCertificateStatus(); + assert.equal(eidasInfo.certificate, certificate); + assert.equal(eidasInfo.status, 'CERTIFICATE_VALID'); // remove the alias from the member, so it can be re-used next time the test is run await member.removeAlias(aliases[0]); }); + + it('recover and verify business member', async () => { + const keyPair = forge.pki.rsa.generateKeyPair(); + const tppAuthNumber = Util.generateNonce(); + const cert = VerifyEidasSample.generateCert(keyPair, tppAuthNumber); + const verifiedTppMember = await VerifyEidasSample.verifyEidas( + cert, tppAuthNumber, 'gold', keyPair.privateKey); + const recoveredMember = await VerifyEidasSample.recoverEidas( + verifiedTppMember._id, tppAuthNumber, cert, keyPair.privateKey); + const verifiedAliases = await recoveredMember.aliases(); + assert.equal(verifiedAliases.length, 1); + assert.equal(verifiedAliases[0].value, tppAuthNumber); + assert.equal(verifiedAliases[0].type, 'EIDAS'); + }); + + it('member should register with eidas cert', async () => { + const keyStore = new MemoryKeyStore(); + const keyPair = forge.pki.rsa.generateKeyPair(); + const authNumber = Util.generateNonce(); + const cert = VerifyEidasSample.generateCert(keyPair, authNumber); + const member = await VerifyEidasSample.registerWithEidas(keyPair, + keyStore,'gold', cert); + const aliases = await member.aliases(); + const keys = await member.keys(); + const publicKeyDer = forge.pki.pemToDer( + forge.pki.publicKeyToPem(keyPair.publicKey)); + const publicKey = forge.util.encode64(publicKeyDer.data) + .replace(/\+/g, '-').replace(/\//g, '_'); + assert.equal(keys[0].publicKey, publicKey); + assert.equal(keys[0].level, 'PRIVILEGED'); + assert.equal(aliases[0].value, authNumber); + }); });