Encrypt string using RSAES-OAEP with SHA-256 and MGF1 hash functions

I need encrypt a string ( REST API access token ) with a certificate using RSAES-OAEP (RSA Encryption Scheme - Optimal Asymmetric Encryption Padding) with SHA-256 and MGF1 hash functions. I am getting the PEM-encoded X.509 certificate (at least it looks like PEM judging by the beginning) from here https://api-test.ksef.mf.gov.pl/api/v2/security/public-key-certificates .
Was thinking of using Cryptonite but it only supports .pfx (PKCS#12) cartificate which requires a private key and I don’t have a private key for this certificate.

I tried importing the key but I’m getting “Error in Cryptonite.ImportKey.2(): Could not import the key onto the machine, CryptImportKey API failed. [1628]
Windows CryptoAPI error -2146893817: 80090007h: Bad Version of provider.”. Unfortunately I have to use that specific key.

My Clarion code:

PublicKeyLen         LONG                                  ! 
PublicKey            STRING(4096)                          ! 
PublicKey64          STRING(4096)                          ! 
CODE
! Import Public Key  
PublicKey64 = 'MII....iL5l1VxbU='    ! This is the whole key from url above 
str.SetValue(PublicKey64,st:clip)
str.Base64Decode() 
PublicKeyLen = str.Length()
PublicKey = str.GetValue()
If Crypto.ImportKey(PublicKey, PublicKeyLen, Crypto.hExchangeKey)  <> Crypto:OK
  stop('Import Failed')
End

I have this working Python script that does that

import base64
import hashlib
import logging
import os
import time
from dataclasses import dataclass

import requests
from typing import Optional
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding as asym_padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes

    def _get_encryption_public_key(self):
        """
        Returns:
            RSA key (cryptography).

        Raises:
            RuntimeError: Przy błędzie HTTP lub braku certyfikatu.
        """
        url = f"{self.base_url}/security/public-key-certificates"
        response = requests.get(url, headers={"Accept": "application/json"}, timeout=30)
        if response.status_code != 200:
            raise RuntimeError(
                f"Błąd pobierania certyfikatów: HTTP {response.status_code} – {response.text}"
            )
        certs = response.json()
        try:
            token_cert = next(c for c in certs if "KsefTokenEncryption" in c["usage"])
        except StopIteration:
            raise RuntimeError("Brak certyfikatu KsefTokenEncryption w odpowiedzi API")

        cert_der = base64.b64decode(token_cert["certificate"])
        cert = x509.load_der_x509_certificate(cert_der, default_backend())
        return cert.public_key()

    def _encrypt_token(self, timestamp_ms: int, public_key) -> str:
        """
        Encrypts token in format "{token}|{timestampMs}" with RSA-OAEP SHA-256 key.

        Args:
            timestamp_ms: Timestamp from challenge response (Unix ms).
            public_key: public key RSA MF.

        Returns:
            str: Encrypted Base64 encoded token.
        """
        token_string = f"{self.api_token}|{timestamp_ms}"
        encrypted = public_key.encrypt(
            token_string.encode("utf-8"),
            padding.OAEP(
                mgf=padding.MGF1(algorithm=hashes.SHA256()),
                algorithm=hashes.SHA256(),
                label=None,
            ),
        )
        return base64.b64encode(encrypted).decode("ascii")```

How can this be accomplished in Clarion? Any syggestions?

Maybe rather than import the public key into the certificate store you could try to use it as a file.

With a little editing, the JSON text in your link can translate into a certificate that has the public key. You can see from the screen shot that I was able to import it. (I imported two certs from that file, with different thumbprints.)

You can use openssl to manipulate certificate and key files into various formats.

Another option for encrypting with Clarion are various utility encryption functions in NetTalk.

I am saving these certificates to text files using jFiles and StringTheory methods

LOOP x# = 1 TO MyJSON.Records()
	CertRow &= MyJSON.Get(x#)
	StrResponse.SetValue(CertRow.GetValueByName('certificate'))
	CASE CertRow.GetValueByName('usage') 
	OF 'KsefTokenEncryption'
		StrResponse.SaveFile('.\Cert\KsefTokenEncryption.crt')
	OF 'SymmetricKeyEncryption'
		StrResponse.SaveFile('.\Cert\SymmetricKeyEncryption.crt')
	END
END

I can import these certificate files into certificate store manually too, but the official API documentation says these can change unexpectedly and recommends to check them upon every request or at least once a day. Therefore I would rather fetch them at the beginning of each session.

NetEncryptString from NetTalk only supports CALG_AES_256 which is symetric. I need to use asymetric RSAES-OAEP which is the current standard for modern API authentication. What I need is to find a way to encrypt my string which consists of api token and a timestamp string with RSAES-OAEP using public key saved on disk, stored in StringTheory object or string variable. Basically Clarion equivalent of this Python code

        encrypted = public_key.encrypt(
            token_string.encode("utf-8"),
            padding.OAEP(
                mgf=padding.MGF1(algorithm=hashes.SHA256()),
                algorithm=hashes.SHA256(),
                label=None,
            ),
        )
        return base64.b64encode(encrypted).decode("ascii")```

Speaking only to the problem of getting the public key, if you have saved the certificate to a disk file you can run openssl.exe to extract the public key to a text file. (Using ShellExecute, or OddJob, etc.)

Mr. Google says openssl can do RSA-OAEP with its pkeyutl command, but I have no personal experience.

This also might be an interesting question for the Wednesday clarionLive webinar.

Sorry, maybe the confusion comes from the fact that I’m using terms certificate and public key synonymously.
Well, actually the difference between Public Key and a Certificate is that Certificate includes the Public Key (for encryption), the owner’s information (subject), validity dates, and the digital signature of the issuer (Certificate Authority). Certificate Authority certificates are added to your certificate store so you OS knows they can be trusted. So it’s a matter of trust not ability to encrypt. You can have self signed Certificate or Public Key and still use it to encrypt data. If you publish your public key I can encrypt data with it and you can decrypt using your private key. That’s what asymmetric encryption is about. They don’t have to be signed as long as I trust it’s yours. As as far as encryption is concerned it makes no difference. You can encrypt using Public Key or Certificate regardless if it’s trusted or not.
The certificate in question is issued by our government so it’s signed and trusted, so that businesses can verify they encrypt their data and send it assured that only the issuer can decrypt it.

The problem is not about that though, it’s about being able to use this encryption algorithm and hash functions in Clarion. I just wrote a php script today that reads the Certificate file fetched by Clarion and saved to .\Cert\KsefTokenEncryption.crt and encrypts my token string.

<?php
require __DIR__ . '/vendor/autoload.php';
use phpseclib3\Crypt\PublicKeyLoader;
use phpseclib3\Crypt\RSA;
$data = "This is me token String";
$certPem = file_get_contents('.\Cert\KsefTokenEncryption.crt');
$publicKey = PublicKeyLoader::load($certPem);
$publicKey = $publicKey->withPadding(RSA::ENCRYPTION_OAEP)
                       ->withHash('sha256')
                       ->withMGFHash('sha256');
$ciphertext = $publicKey->encrypt($data);
echo base64_encode($ciphertext);
echo "\n" ;
?>

The problem is how to do that in Clarion. Worst case scenario I will create API endpoint doing that in Python or PHP an will call it from Clarion app, but that solutiom is a bit ugly.

Note this is used in majority of government and bank API so connecting to these will not be possible without this.