Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion app/sample/CreateTransferTokenWithUnusualOptionsSample.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 12 additions & 12 deletions core/src/http/AuthHttpClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
45 changes: 45 additions & 0 deletions core/src/http/HttpClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
27 changes: 27 additions & 0 deletions core/src/security/Crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
*
Expand All @@ -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));
}
Expand Down
28 changes: 28 additions & 0 deletions core/src/security/engines/KeyStoreCryptoEngine.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand All @@ -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.
*
Expand Down
25 changes: 25 additions & 0 deletions core/src/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -360,7 +375,17 @@ export type GetEidasVerificationStatusResponse = {
statusDetails: string,
};

export type GetEidasCertificateStatusResponse = {
status: EidasCertificateStatus,
certificate: string,
};

export type WebhookConfig = {
url: string,
type: Array<EventType>,
};

export type RegisterWithEidasPayload = {
bankId: string,
certificate: string,
};
137 changes: 137 additions & 0 deletions tpp/sample/VerifyEidasSample.js
Original file line number Diff line number Diff line change
@@ -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 {
/**
Expand Down Expand Up @@ -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;
Loading