Signatures

How to generate a digital signature

Our Open Banking APIs mandate the use of a digital signature on all endpoints of the Xs2a Module.

Digital signatures are implemented as described by the NextGetPSD2 XS2A Framework (version 1.3 including Errata) which in turn references the Signing HTTP Messages draft version 10.

It should be noted that the NextGenPSD2 XS2A Framework version 1.3 Errata document specifies significant changes to the signing algorithm, namely that the Date has been removed from the headers to be signed and normalisation of headers have been made explicit (lower case letters for signed headers).

The below example demonstrates usage of the Forge Javascript cryptography library to sign a HTTP request. This code may be used to validate your signing algorithm but it should never be used in a production environment. Please take care not to expose your eIDAS QSEALC certificate's private key to a web browser or otherwise be made publicly available.

var pki = forge.pki;
var sha256 = forge.md.sha256;
var rsa = pki.rsa;

var keyId = ...your CA certificate keyId;
var headers = ...your HTTP request headers;
var privateKey = ...your signing certificate privateKey in PEM format;

// Add digest header.
jqXhr.setRequestHeader('Digest', calculateMessageDigest(settings.data, headers));
// Add signature header.
jqXhr.setRequestHeader('Signature', calculateSignature(keyId, headers, privateKey));

function calculateMessageDigest(data) {
  return "SHA-256=" + btoa(sha256.create().update(data || []).digest().data);
}

function calculateSignature(keyId, headers, privateKey) {
  var headerNames = ['digest', 'x-request-id'];
  var algorithm = 'rsa-sha256';
  var pk = pki.privateKeyFromPem(privateKey);
  var signature = btoa(pk.sign(sha256.create().update(getSigningString(headers, headerNames))));
  return 'keyId="' + keyId + '",'
      + 'algorithm="' + algorithm + '",'
      + 'headers="' + headerNames.join(' ') + '",'
      + 'signature="' + signature + '"';
}

function getSigningString(headers, headerNames) {
  // map headers in the http request to lowercase, before matching with allowed signature headernames (which are in lower-case)
  var key, keys = Object.keys(headers);
  var n = keys.length;
  var lowerCaseHeaders={};
  while (n--) {
    key = keys[n];
    lowerCaseHeaders[key.toLowerCase()] = headers[key];
  }

  var lines = [];
  $(headerNames).each(function (i, headerName) {
    if (lowerCaseHeaders[headerName]) {
      lines.push(headerName + ": " + lowerCaseHeaders[headerName]);
    }
  });
  return lines.join('\n');
}

Parameter keyId calculation

The Berlin Group v1.3 specifies the following for the keyId parameter:

It shall be formatted as follows:
keyId="SN=XXX,YYYYYYYYYYYYYYYY"
where “XXX" is the serial
number of the certificate in
hexadecimal coding given in
the TPP-Signature-CertificateHeader and
"YYYYYYYYYYYYYYYY" is
the full Distinguished Name of
the Certification Authority
having produced this
certificate

Our implementation requires that the full Distinguished Name of
the Certification Authority be formatted in RFC1779 format as demonstrated by this code example. This is not clearly specified by the NextGenPSD2 specification and there is no intention to change our implementation at this time.

The url-encoding of the Certification Authority is only supported when the issuer name of the certificate contains non-ascii characters. So when the certificate was issued with an issuer name of all ascii characters, the Certification Authority in the request will not be url-decoded.

The Distinguished Name of the Certification Authority in RFC1779 format should look something like this, note that properties must be seperated by comma followed by space. The ordering of the properties is specific to your certificate and may differ from what is displayed here.

CN=CA PSD2 Seal, O=Test Certification Authority, OID.2.5.4.97=VATNL-0123456789, C=NL

The following Java code shows how to obtain this value from your QSEAL certificate

X509Certificate x509Certificate = ...;
String dn = x509Certificate.getIssuerX500Principal().getName(X500Principal.RFC1779)