Banque Raiffeisen

Raiffeisen XS2A (Berlin Group) CBPII Extension

Version:  1.20020190215.037.015
State:  Published
Environment: Production
Base URI: https://ccralull-psd2api.luxhub.com/bg/v2
Authorization Endpoint: https://ccralull-cb.luxhub.com/api/oauth/authorize
Token Endpoint: https://ccralull-psd2api.luxhub.com/api/oauth/token
Categories: PSD2
Passport :

Security

eIDAS Certificates considerations

The eIDAS certificates referred to in this guide are SSL certificates with dedicated protection profiles that allows to be used in the PSD2 context. To achieve the PSD2 security requirements, banks and PSD2 service providers will use Qualified Certificates for Websites and Qualified Certificates for Electronic Seals. Those certificates will be issued by Qualified Trust Service Providers (QTSPs) based on the new technical standard, ETSI TS 119 495, which was published in May 2018. Qualified Certificates enable identification and verification of the payment institution by a third party. Identification will be based on the legal name of an organization, registration number and its main role(s) in the payments space.

There are two types of such certificates:

QWAC (Qualified Website Authentication Certificate): used as Client Certificates in MA-TLS

QSeal (Qualified Certificate for Seals) : used to sign requests using http-signature

All PSD2 APIs require both types of certificates, QWAC to access the API and QSeal for http-signature, i.e. message signing.

LUXHUB implementation of certificates

Our PSD2 APIs are protected by Mutual TLS protocols based on eIDAS Certificates, as required by the PSD2 European Directive. This means that if you want to access one of our PSD2 APIs, you need to use an eIDAS TLS Client Certificate for your requests. If you don’t have such a certificate, you can download a mock eIDAS Certificate from our Developer Portal (Log in, then go to Applications -> select Application -> Download QWAC Certificate). Our mock certificate nevertheless only allow access to Sandbox APIs. To access Production, you need to get your own PSD2 eIDAS Certificate from a Qualified Trust Service Provider.

At the application level, the PSD2 APIs require message signing following http-signature specification, with the signing QSeal certificate (different from the one used as client Certificate). To access the Sandbox, we also provide you such a mock certificate for message signing. The "download QSeal certificate button" is located just next to the one for downloading a QWAC Certificate.

Access Management

Our APIs use OAuth2 as authorization mechanism for access management to the endpoints. The main points related to the implementation of OAuth2 in the context of PSD2 APIs are presented below.

API Authorization

Berlin Group specifications do not impose any specific authorization framework to be used, whereas STET specifications specifically mentioned OAuth2 as authorization mechanism. Therefore, LUXHUB decided to employ state of the art authorization by protecting all endpoints via OAuth2 authorization. Please consult RFC 6749 for more details about this authorization framework.

Authorization Code Grant

The most used flow is Authorization Code grant, which allows PSU to authorize (give consent) to its resources in a secure manner. The authorization code provides a few important security benefits, such as the ability to authenticate the client, as well as the transmission of the access token directly to the client without passing it through the resource owner’s user-agent and potentially exposing it to others, including the resource owner. [https://tools.ietf.org/html/rfc6749#page-24]

Client Credentials Grant

There are few endpoints where the Client Credentials flow is also allowed, in which cases the respective resources to which it is granted access are considered completely detached from the PSU until they are authorized by him - they are considered to belong to the TPP up to this moment.

Client credentials are used as an authorization grant typically when the client is acting on its own behalf (the client is also the resource owner) or is requesting access to protected resources based on an authorization previously arranged with the authorization server. [https://tools.ietf.org/html/rfc6749#page-40]

Proof Key for Code Exchange

As an additional security measure to protect the Authorization Code Grant in OAuth2 flow, LUXHUB is implementing Proof Key for Code Exchange (PKCE) as described in [https://tools.ietf.org/html/rfc7636]. This mitigates the threat of having the authorization code intercepted. The technique involves the client first creating a secret, and then using that secret again when exchanging the authorization code for an access token. This way if the code is intercepted, it will not be useful since the token request relies on the initial secret.

Registering your application for API access

To access an API you need to obtain your OAuth2 credentials (client_id and client_secret) and use them to get tokens within the scope needed for the endpoint you are trying to access. There are two options to do so:

  • Dynamic registration based on eIDAS Certificate

For all the PSD2 APIs, we provide OAuth2.0 Dynamic Client Registration endpoints. You just need to call the /register endpoint of the Third Party Management API, using your QWAC eIDAS certificate. In the request, you will have to provide the required redirect URI to your application, where the customer will be redirected after authentication. In the response, you will receive the credentials that you will need to use in order to get details about the API exposed on the LUXHUB platform and register access for your application to the ones that you deem interesting.

This Dynamic Client Registration for TPPs will be available, for now, only in Production environment. Details about how it is to be used are available in Onboarding via API paragraph in this document.

  • Use our Developer Portal

If you don’t have an eIDAS Certificate yet or if you want to create more than one set of OAuth Credentials, you can use the Developer Portal for authorizing your application. If you don’t have an account yet, you need to register first (see How to register section above). Once you are logged in, you can go to the Applications section and create a new application.

Credentials management and API access is managed at application level, i.e. each application will receive a pair of (client_id, client_secret) credentials and will have to register the APIs to which it requires access.

You can change the APIs supported by your application at any time, but keep in mind that you have to do two important - immutable - choices:

Environment: You cannot mix Production APIs with Sandbox APIs

Category: As of now ONLY applications using PSD2 API can be created, in the near future non-PSD2 APIs will be offered as well. The current documentation will be updated to reflect this change once available.

When creating an application you also have to provide the redirect URLs of your application. This is where your application’s users (named Payment Service User - PSU - in PSD2 lingo) will be redirected after doing strong customer authentication (SCA) in the bank realm. Depending on your implementation, you may implement several redirect URLs (for example different redirect URls for account information consent approval and for payment authorization).

The redirect URLs defined here will have to match the URLs provided when the application calls/api/oauth/authorize endpoint (details in sections below).

Finally you will have to upload an X.509 Certificate, which will help us generate a PSD2-compliant OAuth2 client_id. In Production, you have to use your own eIDAS Certificate granted by a QTSP - the same certificate used to call the bank’s API. In Sandbox, we will provide you with a mock certificate that you can use. You can download your certificate from the Applications page. Binding a real eIDAS certificate to your application via the portal is also supported in Sandbox, in which case the said certificate needs to be uploaded.

Message signing 

Berlin Group standard

Berlin Group specification does not require ASPSP to enforce message signing. However, LUXHUB considered this as best practice that should be enforced for all communication.

Therefore, all requests sent by TPPs to an ASPSP hosted on LUXHUB platform will have to be signed at application level using the http-signature protocol as defined in [https://tools.ietf.org/html/draft-cavage-http-signatures-10].

The electronic signature of the TPP has to be based on a qualified certificate for electronic seals. This qualified certificate has to be issued by a qualified trust service provider according to the eIDAS regulation. The content of the certificate has to be compliant with the requirements of EBA RTS on SCA and CSC. The certificate of the TPP has to indicate all roles the TPP is authorized to use. The corresponding public key will be present as a header in all the requests made towards the API.

For more details on this subject please see Section 4.2 in Berlin Group NextGenPSD2 XS2A Framework Implementation Guidelines.

For signing your requests in the case of using mock QSeal certificates downloaded from LUXHUB Developer Portal, please use the certificate named QSealC-cert.pem and the key named QSealC-key.pem.

Signing implementation details

For Berlin Group http-signing details, please refer chapter “12.2 Requirements on the "Signature" Header” of the XS2A Implementation Guide version 1.3. It describes how signature header should be interpreted.

LUXHUB implementation of the http-signature is using RFC2253 for formatting the distinguished name of the CA that issued the certificate used for signing - as part of keyId element of signature header.

For example, considering Java as implementation language, and reffering X500Principal class documentation, the correct code for implementing the keyid parameter should be:

 

String keyId = "SN=" + cert.getSerialNumber().toString(16) + ", CA=" + cert.getIssuerX500Principal().getName(); //default behavior
// or
String keyId = "SN=" + cert.getSerialNumber().toString(16) + ", CA=" + cert.getIssuerX500Principal().getName(X500Principal.RFC2253);

 

Request example - as per chapter “12.2 Requirements on the "Signature" Header” of the XS2A Implementation Guide version 1.3

  • assume the following payment initiation request :
POST https://api.testbank.com/v1/payments/sepa-credit-transfers
        Content-Type: application/json
        X-Request-ID: 99391c7e-ad88-49ec-a2ad-99ddcb1f7721
        PSU-IP-Address: 192.168.8.78
        PSU-ID: PSU-1234
        PSU-User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:54.0) Gecko/20100101 Firefox/54.0
        TPP-Redirect-URI: https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb&code_Cchallenge_Mmethod="S2
                                56"
        Date: Sun, 06 Aug 2017 15:02:37 GMT

        {
          "instructedAmount":
          {
            "currency": "EUR",
            "amount": "123"
          },
          "debtorAccount":
          {
            "iban": "DE2310010010123456789"
          },
          "creditor":
            {
              "name": "Merchant123"
            },
          "creditorAccount":
            {
              "iban": "DE23100120020123456789"
            },
          "remittanceInformationUnstructured": "Ref Number Merchant"
        }
  • so the base64 encoded body will be:
eyAgICANCiAgICJpbnN0cnVjdGVkQW1vdW50IjogeyJjdXJyZW5jeSI6ICJFVVIiLCAiYW1vdW50IjogIjEyMyJ9LA0KICAgImRlYnRvckFjY291bnQiOiB7ImliYW4iOiAiREUyMzEwMDEwMDEwMTIzNDU2Nzg5In0sDQogICAiY3JlZGl0b3IiOiB7Im5hbWUiOiAiTWVyY2hhbnQxMjMifSwNCiAgICJjcmVkaXRvckFjY291bnQiOiB7ImliYW4iOiAiREUyMzEwMDEyMDAyMDEyMzQ1Njc4OSJ9LA0KICAgInJlbWl0dGFuY2VJbmZvcm1hdGlvblVuc3RydWN0dXJlZCI6ICJSZWYgTnVtYmVyIE1lcmNoYW50Ig0KfQ==
  • while the SHA-256 of the body is, in base64:
F9li3V7yu8S/QKVOhWiiiqJBhGMVId8UGZ4sBRVPkok=
  • therefore using rsa-sha256 signing algorithm the signed request of the TPP will be:
POST https://api.testbank.com/v1/payments/sepa-credit-transfers
        Content-Type: application/json
        X-Request-ID: 99391c7e-ad88-49ec-a2ad-99ddcb1f7721
        PSU-IP-Address: 192.168.8.78
        PSU-ID: PSU-1234
        PSU-User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:54.0) Gecko/20100101 Firefox/54.0
        TPP-Redirect-URI: https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb&code_Cchallenge_Mmethod="S256"
        Date: Sun, 06 Aug 2017 15:02:37 GMT
        Digest: SHA256=ZuYiOtZkVxhjWmwTO5lOpsPevUNMezvk6dfb6fVhebM=
        Signature: keyId="SN=9FA1,CA=D-TRUST%20CA%202-1%202015,O=DTrust%20GmbH,C=DE",algorithm="rsa-sha256",headers="digest x-request-id psu-id tpp-redirect-uri date",signature="Base64(RSA-SHA256(signing string))"
        TPP-Signature-Certificate: <TPP's_eIDAS_Certificate>

        {
          "instructedAmount": {"currency": "EUR", "amount": "123"},
          "debtorAccount": { "iban": "DE2310010010123456789"},
          "creditor": { "name": "Merchant123"},
          "creditorAccount": {"iban": "DE23100120020123456789"},
          "remittanceInformationUnstructured": "Ref Number Merchant"
        }

Note: official Berlin Group example is wrong related to the case of "headers" component of keyid which needs to be in lower case only; so each header name needs to be normalized as such.

  • and signing string is:
digest: SHA-256=ZuYiOtZkVxhjWmwTO5lOpsPevUNMezvk6dfb6fVhebM=
x-request-id: 99391c7e-ad88-49ec-a2ad-99ddcb1f7721
psu-id: PSU-1234
tpp-redirect-uri: https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb&code_Cchallenge_Mmethod="S256"
date: Sun, 06 Aug 2017 15:02:37 GMT

 

http-signature code snippets

Below code snippets are provided "as-is" with no express guarantee or licence and free of charge, as a help to third parties that would like to connect to the APIs available on LUXHUB platform. They are known to work at the moment of being presented in this guide. Each third party using these should take it's own due diligence if they plan to use them in production applications.

1. Java
- the Java code below can be used as well from Scala and Groovy

 

//berlin group version
import org.tomitribe.auth.signatures.PEM;
import org.tomitribe.auth.signatures.Signature;
import org.tomitribe.auth.signatures.Signer;...

public class HttpSignatureCalculatorBG implements SignatureCalculator {
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("EEE, dd MMM YYYY HH:mm:ss");
private static final List<String> ALLOWED_HEADERS = Arrays.asList("X-Request-ID", "Date", "Digest");

private String certPath;
private String keyPath;

public HttpSignatureCalculatorBG(String certPath, String keyPath) {
this.certPath = certPath;
this.keyPath = keyPath;
}

@Override
public void sign(Request request) throws Exception {

String date = DATE_FORMAT.format(new Date()) + " GMT";

request.getHeaders().set("Date", date);

if(request.getBody() != null) {
String requestBody = new String(request.getBody().getBytes());
request.getHeaders().set("Digest", generateDigest(requestBody));
} else {
request.getHeaders().set("Digest", generateDigest(""));
}

BufferedReader reader = new BufferedReader( new InputStreamReader(loadFile(certPath) ) );
String certificate = reader.lines().collect( Collectors.joining( System.lineSeparator() ) );

request.getHeaders().set("TPP-Signature-Certificate", formatCertificate(certificate));
request.getHeaders().set("Signature", generateSignature(request));

}

private String formatCertificate(String certificate) {
return certificate.replaceAll("\n", "")
.replaceAll("\r", "")
.replace("-----BEGIN CERTIFICATE-----", "")
.replace("-----END CERTIFICATE-----", "");
}

private String generateDigest(String payload) {
try {
byte[] digest = MessageDigest.getInstance("SHA-256").digest(payload.getBytes());
return "SHA-256=" + new String(Base64.getEncoder().encode(digest));
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException("Wrong algorithm", e);
}
}

private String generateSignature(Request request) throws CertificateException, InvalidKeySpecException, IOException {

Map<String, String> headersMap = new HashMap<String, String>();
List<String> signatureHeaders = new ArrayList<String>();

request.getHeaders().forEach(entry -> {
if(!ALLOWED_HEADERS.contains(entry.getKey())) {
return;
}
headersMap.put(entry.getKey(), entry.getValue());
signatureHeaders.add(entry.getKey());
});

CertificateFactory cf = CertificateFactory.getInstance("X.509");
X509Certificate cert = (X509Certificate) cf.generateCertificate(loadFile(certPath));

String keyId = String.format("SN=%s,CA=%s", cert.getSerialNumber(), cert.getIssuerDN());
String algorithm = "rsa-sha256";

Signature signature = new Signature(keyId, algorithm, null, signatureHeaders);

PrivateKey privateKey = PEM.readPrivateKey(loadFile(keyPath));

Signer signer = new Signer(privateKey, signature);

String method = "method";
String uri = "uri";

Signature signed = signer.sign(method, uri, headersMap);

return signed.toString();

}

private InputStream loadFile(String content) throws FileNotFoundException {
return new ByteArrayInputStream(content.getBytes());
}

}

// stet version
import org.tomitribe.auth.signatures.PEM;
import org.tomitribe.auth.signatures.Signature;
import org.tomitribe.auth.signatures.Signer;...

public class HttpSignatureCalculatorStet implements SignatureCalculator {

    private static final List<String> ALLOWED_HEADERS = Arrays.asList("X-Request-ID", "psu-date", "Digest",
            "PSU-IP-Address", "(request-target)", "content-length", "content-type");

    private String keyPath;
    private String certificateUrl;

    public HttpSignatureCalculatorStet(String keyPath, String certificateUrl) {
        this.keyPath = keyPath;
        this.certificateUrl = certificateUrl;
    }

    @Override
    public void sign(Request request) throws Exception {

        String date = String.valueOf(System.currentTimeMillis());
        String requestTarget = request.getMethod().toString() + " " + request.getUri().getPath();

        if (request.getBody() != null) {
            String requestBody = new String(request.getBody().getBytes());
            request.getHeaders().set("Digest", generateDigest(requestBody));
            request.getHeaders().set("content-length", requestBody.length());
        } else {
            request.getHeaders().set("Digest", generateDigest(""));
        }

        request.getHeaders().set("psu-date", date);
        request.getHeaders().set("PSU-IP-Address", InetAddress.getLocalHost().getHostAddress());
        request.getHeaders().set("(request-target)", requestTarget);

        request.getHeaders().set("Signature", generateSignature(request));

    }

    private String generateDigest(String payload) {
        try {
            byte[] digest = MessageDigest.getInstance("SHA-256").digest(payload.getBytes());
            return "SHA-256=" + new String(Base64.getEncoder().encode(digest));
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalArgumentException("Wrong algorithm", e);
        }
    }

    private String generateSignature(Request request)
            throws CertificateException, InvalidKeySpecException, IOException {

        Map<String, String> headersMap = new HashMap<String, String>();
        List<String> signatureHeaders = new ArrayList<String>();

        request.getHeaders().forEach(entry -> {
            if (!ALLOWED_HEADERS.contains(entry.getKey())) {
                return;
            }

            headersMap.put(entry.getKey(), entry.getValue());
            signatureHeaders.add(entry.getKey());
        });

        String algorithm = "rsa-sha256";

        Signature signature = new Signature(certificateUrl, algorithm, null, signatureHeaders);

        PrivateKey privateKey = PEM.readPrivateKey(loadFile(keyPath));

        Signer signer = new Signer(privateKey, signature);

        String method = request.getMethod().toString();
        String uri = request.getUri().getPath();

        Signature signed = signer.sign(method, uri, headersMap);

        return signed.toString();

    }

    private InputStream loadFile(String content) throws FileNotFoundException {
        return new ByteArrayInputStream(content.getBytes());
    }

}

 

2. Python

- definition of signing string functions:

#berlin group version
def set_bg_signature(self, http_signature_cert, http_signature_key):
    key_id = get_cert_key_id(http_signature_cert)
    algorithm = 'rsa-sha256'
    headers = 'x-request-id date digest'
    signing_string = 'x-request-id: ' + self.x_request_id + '\ndate: ' + self.date + '\ndigest: ' + self.digest
    signature = sign_string(http_signature_key, signing_string, 'sha256')
    signature_header = 'keyId="' + key_id + '",algorithm="' + algorithm + '",headers="' + headers + '",signature="' + signature + '"'
    self.signature = signature_header

#stet version
def set_stet_signature(self, http_signature_cert_url, method, url, query_params, http_signature_key, body=None):
    key_id = http_signature_cert_url
    algorithm = 'rsa-sha256'
    headers = 'psu-ip-address psu-date x-request-id (request-target) digest'
    #if body is empty
    headers = headers + ' content-length content-type'
    request_target = method.lower() + ' ' + url.replace(API_Test_Variables.env, '') + params_str
    signing_string = 'psu-ip-address: ' + self.psu_ip_address + '\npsu-date: ' + self.psu_date + '\nx-request-id: ' + self.x_request_id + '\n(request-target): ' + request_target + '\ndigest: ' + self.digest
    #if body is not empty
    signing_string = signing_string + '\ncontent-length: ' + str(len(json.dumps(body))) + '\ncontent-type: ' + headers['Content-Type']
    signature = sign_string(http_signature_key, signing_string, 'sha256')
    signature_header = 'keyId="' + key_id + '",algorithm="' + algorithm + '",headers="' + headers + '",signature="' + signature + '"'
    self.signature = signature_header

where the sign-string function is:

from OpenSSL import crypto
...
def sign_string(key, signing_string, algorithm):
    pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, open(key).read())
    signature = base64.b64encode(crypto.sign(pkey, signing_string, algorithm))
    return signature

 

3. JavaScript

//berlin group version
const crypto = require('crypto');
generateBGSignature(headers, body) {
    let headerCert = QSEALCert.replace(/\r/g, '');
    headerCert = headerCert.replace(/\n/g, '');
    headerCert = headerCert.replace(/-----[^ ]+ CERTIFICATE-----/g, '');
    headers["TPP-Signature-Certificate"] = headerCert;

    const {
        Certificate
    } = require('@fidm/x509');
    const cert = Certificate.fromPEM(QSEALCert);

    const digest = "SHA-256=" + crypto.createHash('sha256').update(body).digest('base64');
    let signingString = "digest: " + digest + "\n" +
        "x-request-id: " + headers["X-Request-ID"] + "\n" +
        "date: " + headers["Date"];

    let signatureHeaders = "Digest X-Request-ID Date";

    if (typeof headers["PSU-ID"] !== "undefined") {
        signingString += "\nPSU-ID: " + headers["PSU-ID"];
        signatureHeaders += " PSU-ID";
    }

    if (typeof headers["PSU-Corporate-ID"] !== "undefined") {
        signingString += "\nPSU-Corporate-ID: " + headers["PSU-Corporate-ID"];
        signatureHeaders += " PSU-Corporate-ID";
    }

    if (typeof headers["TPP-Redirect-URI"] !== "undefined") {
        signingString += "\nTPP-Redirect-URI: " + headers["TPP-Redirect-URI"];
        signatureHeaders += " TPP-Redirect-URI";
    }

    headers["Digest"] = digest;
    headers["Signature"] = "keyId=\"SN=" + cert.serialNumber +
        this.getCertificateIssuerValues(cert.issuer) +
        ", algorithm=\"rsa-sha256\"" +
        ", headers=\"" + signatureHeaders + "\"" +
        ", signature=\"" + crypto.createSign('SHA256').update(signingString).sign(QSEALKey, 'base64') + "\"";

    return headers;
}

//stet version
const crypto = require('crypto');
generateSTETSignature(headers, method, url, body) {
    headers["Digest"] = "SHA-256=" + crypto.createHash('sha256').update(body).digest('base64');

    let signingString = "(request-target): " + method.toLowerCase() + " " + url;
    let signatureHeaders = "(request-target)";

    for (let key in headers) {
        if (headers.hasOwnProperty(key)) {
            signingString += "\n" + key.toLowerCase() + ": " + headers[key];
            signatureHeaders += " " + key;
        }
    }

    headers["Signature"] = "keyId=\"" + my_url_to_certificate + "\"" +
        ", algorithm=\"rsa-sha256\"" +
        ", headers=\"" + signatureHeaders + "\"" +
        ", signature=\"" + crypto.createSign('SHA256').update(signingString).sign(QSEALKey, 'base64') + "\"";

    return headers;
}
 
 

This website uses cookies. By continuing to use our website, you accept the use of these cookies.