Appearance
PCI DSS integration Beta
Treezor complies with the Payment Card Industry Data Security Standard (PCI DSS) to secure the sensitive data (e.g., PAN, PIN, CVV) of payment cards.
Configuration – Migrating to PCI DSS services
If you've been with Treezor since before PCI DSS became available, you must migrate to these new services. Please contact Treezor for more information.
The implementation of PCI DSS at Treezor relies on the following security principles, which change the way you call the Treezor API:
Principle | Description |
---|---|
Mutual TLS | The Mutual Transport Layer Security (mTLS) protocol guarantees mutual authentication and encrypted communication, preventing interception or identity theft between systems. Applies to all endpoints. |
RSA-PSS signing | HTTP request signing ensures the integrity and authenticity of the data sent, guaranteeing that each request is valid and comes from the authorized source. Applies to PCI DSS-specific endpoints. |
In addition, all your requests transit through the following URL:
- Sandbox –
https://<your_company_name>.preprod.secure.treezor.co
- Production –
https://<your_company_name>.secure.treezor.co
This article takes you through the steps to implement mTLS and sign your requests, focusing on your Sandbox environment. Please bear in mind you'll have to repeat the same procedure for your Production environment.
Setting up your certificates
To use mTLS and sign your requests, you first need to send 2 CSR (Certificate Signing Request) to Treezor for the following certificates:
- mTLS certificate (to set up, with your private key, the TLS negotiation)
- Signature certificate (to sign, with another private key, your PCI DSS endpoints only requests)
In return, Treezor provides the signed certificates for you to configure your client and authenticate your requests.
You need to provide a different set of certificates for each of your Treezor environments (Sandbox and Production), which means you’ll have to send 4 CSR files.
1. Create your RSA keys sensitive data
You need a private key to create your certificate signing request (CSR); you have to create 2 key pairs for your 2 Sandbox CSRs.
These keys must have the following attributes:
- Type: RSA
- Format: PKCS1
- Size: 2048
Configuration – Use a different RSA key for each CSR
Your mTLS certificate and your signature certificate must have different key pairs.
Here are the commands to run for your Sandbox, please keep in mind you'll have to do the same for your Production environment.
openssl genrsa -out <your.company.name>_privatekey_mtls.pem 2048
openssl genrsa -out <your.company.name>_privatekey_signature.pem 2048
1
2
2
Security – Protect your private key
Don’t share your private keys with anyone and make sure they are securely stored (e.g., secure vault, key management system).
2. Create your CSR files
The Certificate Signing Request (CSR) is the request you send to a Certificate Authority (or CA, in this case, Treezor) to apply for a digital identity certificate. The CSR includes the public key and additional information such as the entity requesting the certificate common name (CN).
Here are the commands to run:
openssl req -new -key <your.company.name>_privatekey_mtls.pem -out <your.company.name>_csr_mtls.pem
openssl req -new -key <your.company.name>_privatekey_signature.pem -out <your.company.name>_csr_signature.pem
1
2
2
Then use the information in the table below to complete the CSR information.
Information | Description |
---|---|
Country | The two-letter country code representing the country where the organization is located. |
State / Province | The state or province where the organization is located (e.g., Brittany, IdF). |
Locality | The locality or city where the organization is located. |
Organization Name | The legal name of the organization to which the entity belongs. This could be the company, department, or other organizational unit. |
Organizational Unit | (optional) The specific unit within the organization. For example, "IT Department" or "Marketing". |
Common Name | Usually, the fully qualified domain name (FQDN) for which the certificate is being requested. For example, if the certificate is for a website, the CN might be the domain name (e.g., https://yourcompany.com). |
The email address of the organization. |
3. Ask Treezor to generate your certificates
CSR files and certificates aren't considered sensitive data. This is why you can exchange them by email.
- Send your CSR files to your Treezor Technical Account Manager.
- Treezor will send you back the signed certificates.
Security – Don't send your private keys, only the CSR files
If you were to send us your private key, you'll have to generate new ones and start the process from scratch.
You now have the necessary certificates for mTLS authentication and to sign your PCI DSS requests.
Reading – More information available on mTLS
Learn more about certificates, including when to renew or revoke them.
Sign your requests
Any endpoint handling sensitive information regarding cards has been replaced by PCI DSS-specific alternatives (see PCI DSS endpoints section). These requests require that you sign them in addition to providing the mTLS certificate.
Create your signature
Follow the process to create your signature:
1. Canonical request
Create a character string (e.g., canonical request) made up of 3 elements (with a line break for each element):
- HTTP Verb
- Path?query parameters (in alphabetical order, separate the key=value couples with &)
- Body (the body content in the request must be strictly identical to the one in the signature)
Pseudocode example:
CANONICAL_REQUEST = "<httpVerb>\n<httpPath>?<httpSortedQueryParams>\n<httpBody>"
Canonical signature examples
Request | Canonical signature |
---|---|
GET https://{baseURL}/cards/1234/pan?a=b&c=d&e=f | GET\n/cards/1234/pan?a=b&c=d&e=f\n |
POST https://{baseURL}/cards/tokenize with body = {"cardId": "1234", "userId":"1234"} | POST\n/cards/tokenize\n{"cardId": "1234", "userId":"1234"} |
2. Sign the canonical request
Sign the canonical request obtained in the previous steps by:
- Hashing the character string with the SHA-256 algorithm.
- Signing the hash with the
<your.company.name>_privatekey_signature.pem
using the RSA-PSS algorithm.
3. Encode the signature in base64
Lastly, encode in base64 the signature generated in step 2.
4. Example
The examples below gather all 3 steps to create your signature.
go
package main
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
_ "embed"
"encoding/base64"
"encoding/pem"
"fmt"
"io"
"net/http"
"strings"
)
//go:embed <your.company.name>_privatekey_signature.pem
var pemPrivateKeySignature []byte
// CreateSignature returns signature for the request.
// Based on the HTTP method, the URL, the query params and the body,
// this function creates a string called 'canonical request' and signs it
// with the signature private key using the PSS algorithm.
// Lastly, it base64-encodes the signature.
func CreateSignature(request *http.Request) (string, error) {
requestBody := []byte("")
if request.Body != nil {
var err error
requestBody, err = io.ReadAll(request.Body)
if err != nil {
return "", fmt.Errorf("failed to read request body: %w", err)
}
request.Body = io.NopCloser(strings.NewReader(string(requestBody)))
}
canonicalRequest := fmt.Sprintf("%s\n%s\n%s", request.Method, request.URL.RequestURI(), string(requestBody))
digest := sha256.Sum256([]byte(canonicalRequest))
block, _ := pem.Decode(pemPrivateKeySignature)
if block == nil {
return "", fmt.Errorf("failed to decode private key using pem format")
}
privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return "", fmt.Errorf("failed to parse PKCS1 private key: %w", err)
}
signature, err := rsa.SignPSS(rand.Reader, privateKey, crypto.SHA256, digest[:], &rsa.PSSOptions{})
if err != nil {
return "", fmt.Errorf("failed to sign data: %w", err)
}
return base64.StdEncoding.EncodeToString(signature), nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
php
<?php
use Monolog\Logger;
use phpseclib3\Crypt\RSA;
use phpseclib3\Crypt\RSA\PrivateKey;
const SIGNATURE_KEY_FILE = '<your.company.name>_privatekey_signature.pem';
/**
* CreateSignature returns signature for the request.
* Based on the HTTP method, the URL, the query params and the body,
* this function creates a string called 'canonical request' and signs it
* with the private key signature using the PSS algorithm.
* Lastly, it base64-encodes the signature.
* @throws RuntimeException
*/
function createSignature(string $httpVerb, string $url, array $queryParams, string $body): string
{
$logger = new Logger('fake-logger');
$requestUri = parse_url($url, PHP_URL_PATH);
if (!is_string($requestUri)) {
throw new RuntimeException('Invalid URI');
}
ksort($queryParams);
$queryParamsBuilt = http_build_query($queryParams);
$requestUri = $queryParamsBuilt === '' ? $requestUri : $requestUri . '?' . $queryParamsBuilt;
$canonicalRequest = sprintf("%s\n%s\n%s", $httpVerb, $requestUri, $body);
try {
$private = PrivateKey::loadFormat('PKCS1', file_get_contents(SIGNATURE_KEY_FILE))
->withHash('sha256')
->withMGFHash('sha256')
->withPadding(RSA::SIGNATURE_PSS);
} catch (RuntimeException $exception) {
$logger->error(sprintf('Failed to parse PKCS1 private key %s', $exception->getMessage()));
throw $exception;
}
return base64_encode($private->sign($canonicalRequest));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
Make your requests
Now that you have your certificates, you can populate the 2 additional parameters to provide in the headers for your PCI DSS-specific requests (see PCI DSS Swagger).
Header param. | Value | Description | PCI only |
---|---|---|---|
Authorization | Bearer {accessToken} | The type of token followed by your previously generated JWT. See the Authentication article. | |
X-Trz-Client-Certificate | {base64Cerficate} | Your base64-encoded signature certificate in the Base64(Pem(X509)) format (<your.company.name>_csr_signature.pem ). | |
X-Trz-Client-Signature | {signature} | Your base64-encoded signature as previously created. |
go
package main
import (
"crypto/tls"
_ "embed"
"encoding/base64"
"fmt"
"io"
"net/http"
)
var (
//go:embed <your.company.name>_csr_mtls.pem
pemCertificateMTLS []byte
//go:embed <your.company.name>_privatekey_mtls.pem
pemPrivateKeyMTLS []byte
//go:embed <your.company.name>_certificate_signature.pem
pemCertificateSignature []byte
)
func GenerateRequest(httpMethod, requestURL string, queryParams map[string]string,
jwtToken, scaProof string, body io.Reader) (*http.Request, error) {
if jwtToken == "" {
return nil, fmt.Errorf("missing JWT Token")
}
request, err := http.NewRequest(httpMethod, requestURL, body)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
if len(queryParams) > 0 {
q := request.URL.Query()
for key, value := range queryParams {
q.Add(key, value)
}
request.URL.RawQuery = q.Encode()
}
encodedSignature, err := CreateSignature(request)
if err != nil {
return nil, fmt.Errorf("failed to create signature: %w", err)
}
request.Header["X-Trz-Client-Signature"] = []string{encodedSignature}
request.Header["X-Trz-Client-Certificate"] = []string{base64.StdEncoding.EncodeToString(pemCertificateSignature)}
request.Header["Authorization"] = []string{"Bearer " + jwtToken}
if scaProof != "" {
request.Header["X-Trz-SCA"] = []string{scaProof}
}
return request, nil
}
// SendUsingMTLS sends the given HTTP request using mTLS protocol.
func SendUsingMTLS(request *http.Request) (*http.Response, error) {
clientCertificate, err := tls.X509KeyPair(pemCertificateMTLS, pemPrivateKeyMTLS)
if err != nil {
return nil, fmt.Errorf("failed to load x509 key pair: %w", err)
}
httpClient := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
Certificates: []tls.Certificate{clientCertificate},
},
},
}
return httpClient.Do(request)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
php
<?php
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Request;
const MTLS_CERT_FILE = '<your.company.name>_csr_mtls.pem';
const MTLS_KEY_FILE = '<your.company.name>_privatekey_mtls.pem';
const SIGNATURE_CERT_FILE = '<your.company.name>_csr_signature.pem';
function generateRequest(string $httpVerb, string $url, array $queryParams, string $jwtToken, string $scaProof, string $body): Request
{
if ($jwtToken === '') {
throw new RuntimeException('missing JWT Token');
}
$signature = createSignature($httpVerb, $url, $queryParams, $body);
if ($signature === '') {
throw new RuntimeException('Unable to generate signature');
}
$headers = [
'X-Trz-Client-Certificate' => base64_encode(file_get_contents(SIGNATURE_CERT_FILE)),
'X-Trz-Client-Signature' => $signature,
'X-Trz-SCA' => $scaProof,
'Authorization' => "Bearer $jwtToken",
'Content-Type' => 'application/json',
];
return new Request($httpVerb, $url . '?' . http_build_query($queryParams), $headers, $body);
}
/**
* @throws \Psr\Http\Client\ClientExceptionInterface
*/
function sendUsingMTLS(Request $request)
{
$client = new Client([
GuzzleHttp\RequestOptions::CERT => [MTLS_CERT_FILE, ''],
GuzzleHttp\RequestOptions::SSL_KEY => [MTLS_KEY_FILE, ''],
]);
return $client->sendRequest($request);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
Verify your RSA-PSS signature integrity
Treezor provides an endpoint for you to verify your signature in Sandbox
only.
While using this endpoint is optional, it can prove useful to understand why your signature doesn't work when such cases arise.
To do so, replace the PCI DSS {baseURL}
of your request with the one specifically provided by Treezor for the verification, which is built as follows:
https://<your-company-name>.verify.preprod.secure.treezor.co
Information – The verification endpoint isn't mTLS compatible
The verification endpoint doesn't use mTLS, to make your signature testing uncorrelated from the mTLS certificate.
go
package main
import (
"encoding/base64"
"fmt"
"io"
"log"
"net/http"
)
// VerifySignature sends a PCI request to the verification server to debug the signature verification.
func VerifySignature() {
request, err := GenerateRequest(
http.MethodGet,
"https://<your_company_name>.verify.preprod.secure.treezor.co/cards/1234/pan",
map[string]string{
"algorithm": "OPENPGP_ARMOR",
"userId": "1234",
"withCVV": "true",
"userPublicKey": base64.StdEncoding.EncodeToString([]byte("cardholder's OpenPGP public key value")),
"sca": "sca-proof-value",
},
"jwtToken",
"scaProof",
nil,
)
if err != nil {
log.Fatalf("failed to generate request: %s", err)
}
response, err := http.DefaultClient.Do(request)
if err != nil {
log.Fatalf("failed to send request: %s", err)
}
body, err := io.ReadAll(response.Body)
if err != nil {
log.Fatalf("failed to read body response: %s", err)
}
// Human readable result
fmt.Println(string(body))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
php
<?php
use GuzzleHttp\Client;
use Monolog\Logger;
use Psr\Http\Client\ClientExceptionInterface;
/**
* VerifySignature sends a PCI request to the verification server to debug the signature verification.
*/
function verifySignature(string $jwtToken, string $scaProof): void
{
$logger = new Logger('fake-logger');
try {
$request = generateRequest(
'GET',
'https://<your_company_name>.verify.preprod.secure.treezor.co/cards/1234/pan',
[
'algorithm' => 'OPENPGP_ARMOR',
'userId' => '5678',
'withCVV' => true,
'userPublicKey' => base64_encode("cardholder's OpenPGP public key value"),
'sca' => $scaProof,
],
$jwtToken,
$scaProof,
''
);
} catch (RuntimeException $exception) {
$logger->error(sprintf('Failed to generate request %s', $exception->getMessage()));
return;
}
$client = new Client([]);
try {
$response = $client->send($request);
} catch (ClientExceptionInterface $exception) {
$logger->error(sprintf('Failed to send request %s', $exception->getMessage()));
return;
}
// Human readable result
echo $response->getBody()->getContents();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
Outputs the following, with information about what is wrong in the certificate or signature when a KO
value is returned.
yaml
certificate:
filled: OK
base64encoded: OK
pemencoded: OK
parsed: OK
treezorsigned: OK
certhasrsapubkey: OK
signature:
filled: OK
base64encoded: OK
verified: OK
expectedcanonicalrequesttohash: GET\n/cards/1234/pan?algorithm=OPENPGP_ARMOR&sca=sca-proof-value&userId=1234&userPublicKey=Y2FyZCdzIGhvbGRlciBvcGVuIHBncCBwdWJsaWMga2V5IHZhbHVl&withCVV=true\n
expectedhashtobesigned: d9653c56e6ed16a01893f5d4c961ce2d5ac9cf95bab391ecc08a87b74ba37f11
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
yaml
certificate:
filled: OK
base64encoded: OK
pemencoded: OK
parsed: OK
treezorsigned: OK
certhasrsapubkey: OK
signature:
filled: OK
base64encoded: 'KO: your signature in "X-Trz-Client-Signature" header is not well base 64 encoded'
verified: CheckNotPerformed
expectedcanonicalrequesttohash: GET\n/cards/1234/pan?algorithm=OPENPGP_ARMOR&sca=sca-proof-value&userId=1234&userPublicKey=Y2FyZCdzIGhvbGRlciBvcGVuIHBncCBwdWJsaWMga2V5IHZhbHVl&withCVV=true\n
expectedhashtobesigned: d9653c56e6ed16a01893f5d4c961ce2d5ac9cf95bab391ecc08a87b74ba37f11
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
Below the descriptions of each check made, based on the X-Trz-Client-Signature
and X-Trz-Client-Certificate
.
Attribute | Validates that |
---|---|
certificate .filled | Certificate has been provided. |
certificate .base64encoded | Certificate is properly base64-encoded. |
certificate .pemencoded | Certificate is properly PEM-encoded. |
certificate .parsed | Certificate is x509 formatted. |
certificate .treezorsigned | Certificate has been signed by Treezor. |
certificate .certhasrsapubkey | Certificate contains a valid RSA public key. |
signature .filled | Signature has been provided. |
signature .base64encoded | Signature is properly base64-encoded. |
signature .verified | Signature is valid. |
Each of these indicate OK
when the value is as expected, or KO
followed by the encountered error otherwise.
In addition, the following fields are provided:
Attribute | Description |
---|---|
expectedcanonicalrequesttohash | Canonical request calculated from the server side, allowing Treezor to calculate the hash. |
expectedhashtobesigned | Sha256 hash of the expected canonical request to hash. |
PCI DSS Endpoints
The PCI DSS endpoints are the ones for which you need to sign your requests. They can only be used for cards created or migrated to PCI DSS services, and replace their equivalent in this Swagger.
Please keep in mind that all your requests transit through the following URL:
- Sandbox –
https://<your_company_name>.preprod.secure.treezor.co
- Production –
https://<your_company_name>.secure.treezor.co
PCI DSS endpoint | Replaced endpoints |
---|---|
/cards Create a Card. Use the medium field to create Virtual or Physical cards. | Create Virtual Card Create Physical Card |
/cards/{cardId}/assignUser Assign the card to another user. | Reassign Card (partial replacement) |
/cards/{cardId}/tokenize Tokenize the card. | |
/cards/{cardId}/cardImage Rebuild the card image when changing card design, company name, or when retrieving the image results in a 404. Not necessary when initially creating a Virtual Card. | Regenerate Card |
/cards/{cardId}/cardImage Download the card encrypted image. Requires encryption, see Display sensitive data article. | Retrieve Image |
/cards/{cardId}/pan Retrieve the card PAN & CVV. Use the withCVV field to get the encrypted CVV too. Requires encryption, see Display sensitive data article. | |
/cards/{cardId}/changePIN Change the PIN code knowing the current one. | Change PIN |
/cards/{cardId}/setPIN Overwrite the PIN code. | Set PIN |
/cards/{cardId}/pin Retrieve the PIN code of the card. Requires encryption, see Display sensitive data article. | |
/cards/{cardId}/renew Renew the card manually. | Renew Card |
/cards/{cardId}/replace Replace a physical card with the same card data. | |
/cards/{cardId}/inappcryptogram/mastercard/apple-pay Generate an Apple Pay cryptogram for Mastercard digitization process. | Request issuerInitiatedDigitizationData |
/cards/{cardId}/inappcryptogram/mastercard/google-pay Generate a Google Pay cryptogram for Mastercard digitization process. | Request issuerInitiatedDigitizationData |
/cards/{cardId}/inappcryptogram/visa/apple-pay Generate an Apple Pay cryptogram for Visa digitization process. | |
/cards/{cardId}/inappcryptogram/visa/google-pay Generate a Google Pay cryptogram for Visa digitization process. | |
/inappcryptogram/{credentials} Retrieve digitization cryptogram. | |
/users Create a user. | Create User |
API – Swagger documentation available
For a complete list of attributes for these endpoints, check the PCI DSS Swagger.