This shows you the differences between two versions of the page.
|
ac:laboratoare:11 [2024/01/18 14:05] marios.choudary |
ac:laboratoare:11 [2025/11/06 10:36] (current) marios.choudary |
||
|---|---|---|---|
| Line 1: | Line 1: | ||
| - | ===== Lab 11: EMV crypto - Verifying a DDA Signature ===== | + | ===== Lab 6b - TOFU-based Authenticated Key Exchange ===== |
| - | ==== Install python tools to work with your smartcard ==== | + | In this lab, we will implement Trust On First Use in a manner similar to that of SSH. We will base our implementation around ''%%PyCryptodome%%'' - you can find the relevant documentation [[https://www.pycryptodome.org/src/api |here]]. |
| - | Do the following on Linux (this is for Ubuntu/Debian -- you might need root access): | + | ==== 0. Init ==== |
| - | * Install pcsclite-dev: | + | |
| + | Use these commands to generate a private key and a Diffie-Hellman parameter file: | ||
| <code> | <code> | ||
| - | sudo apt-get install libpcsclite-dev | + | openssl genrsa -out private.pem 2048 |
| + | openssl dhparam -out dhparam.pem 2048 | ||
| </code> | </code> | ||
| - | * Then also install these packages: | ||
| - | <code> | ||
| - | sudo apt-get install swig python3-dev libudev-dev python3-pip | ||
| - | </code> | ||
| - | * Get and install Pyscard using pip (install pip if needed) | ||
| - | <code> | ||
| - | pip3 install pyscard | ||
| - | </code> | ||
| - | * Install Pyserial | ||
| - | <code> | ||
| - | pip3 install pyserial | ||
| - | </code> | ||
| - | If this doesn't work, then get pyserial from [[https://pypi.python.org/pypi/pyserial#downloads|here]] | ||
| - | * Install pcsc related libs: | ||
| - | <code> | ||
| - | sudo apt-get install libusb-dev libccid pcscd libpcsclite1 | ||
| - | </code> | ||
| - | * You might also want to install these additional card tools from here: | ||
| - | <code> | ||
| - | sudo apt-get install libpcsc-perl pcsc-tools | ||
| - | </code> | ||
| - | |||
| - | See details [[http://support.gemalto.com/fileadmin/user_upload/IAM/FAQ/How_to_install_the_PC-Link_reader_on_Linux.pdf|here]]. | ||
| - | For Windows drivers you can check [[https://supportportal.gemalto.com/csm/?id=kb_article_view&sys_kb_id=0adc96844f350700873b69d18110c76a&sysparm_article=KB0016522|here]]. However, we recommend using Linux, as the instructions below apply for the linux installation. | + | The ''%%dhparam%%'' file will be used as a hardcoded ''%%.pem%%'' that contains the necessary Diffie-Hellman parameters to generate our DH keys. These values are generally decided upon by convention and are hardcoded - check [[https://datatracker.ietf.org/doc/html/rfc7919#page-19 |RFC 7919]]. An example which can be found there is ''%%ffdhe2048%%''. |
| - | ==== Get information about your card ==== | + | To better understand its structure, you can use the following commands: |
| - | Try this with your card in the smartcard reader: | + | <code bash> |
| - | <code> | + | openssl dhparam -in dhparam.pem -text -noout |
| - | pcsc_scan | + | openssl asn1parse < dhparam.pem |
| </code> | </code> | ||
| - | This should show you applications on the card. If not, don't worry, we'll do it ourselves below. | + | Create a Python environment or use an existing one, then install the required packages: |
| - | ==== Writing a terminal emulator to interact with your card ==== | + | <code bash> |
| + | python3 -m venv create env | ||
| + | source ./env/bin/activate | ||
| + | pip install --upgrade pip | ||
| + | pip install pycryptodome pyasn1 pyasn1-modules | ||
| + | </code> | ||
| - | Start with files for accessing card data in {{:ac:laboratoare:lab_emv_py3.zip|this}} zip file. | + | We will use the ''%%pyasn1%%'' library to parse the ''%%.pem%%'' file's binarized DER format more conveniently. |
| - | (for Python 2 you may use the code [[https://ocw.cs.pub.ro/courses/_media/ac/laboratoare/lab_emv.zip|here]], but is obsolete | + | |
| - | and no longer mantained). | + | |
| - | Create a file named terminal.txt that will be populated as mentioned below. | + | ==== 1. Implement DH + RSA signature (3p) ==== |
| - | This file should end with a line containing the string '0000000000'. | + | |
| - | After updating this file (see below), we can run the terminal in this manner: | + | Starting from {{:ac:laboratoare:lab_tofu.zip |these files}}, solve the TODO 1 series in ''%%dhe_server.py%%'' and ''%%dhe_client.py%%''. |
| - | <code> | + | |
| - | python3 sclink.py --scterminal terminal.txt gg | + | |
| - | </code> | + | |
| - | ==== Select finantial app ==== | + | On the server side, you should: |
| - | We shall now first select the main financial application on the card via the general `1PAY.SYS.DDF01' file | + | * Send the RSA public key to the client. |
| - | available on some EMV cards followed by selection of the Application ID. See EMV Book 1, sections 11.3 and 12 for details. | + | * Generate a DH key pair (use the data hardcoded in ''%%dhparam.pem%%'') and send the public key to the client. |
| + | * Generate a signature over the RSA and DH public keys (concatenate and hash them, then sign using the RSA private key) and send it to the client. Use [[https://pycryptodome.readthedocs.io/en/latest/src/signature/pkcs1_v1_5.html |this]] as your guide. | ||
| + | * Receive the client's DH public key and generate the shared DH secret. | ||
| + | * Derive a symmetric key from the shared secret. Use [[https://pycryptodome.readthedocs.io/en/latest/src/protocol/kdf.html#hkdf |HKDF]] for this. | ||
| - | <note> | + | On the client side, you should: |
| - | Newer EMV cards may not support the 1PAY.SYS.DDF01 selection method described below, but you may need to use | + | |
| - | the Application ID list method or some other variant, as explained in the EMV Book 1, chapter 12. | + | |
| - | </note> | + | |
| - | In summary, the main steps are these: | + | * Receive the RSA public key from the server. |
| + | * Receive the server's DH public key. | ||
| + | * Receive the server's signature over the RSA and DH public keys and [[https://pycryptodome.readthedocs.io/en/latest/src/signature/pkcs1_v1_5.html |verify]] it using the server's public key. | ||
| + | * Generate a DH key pair and send the public key to the server. | ||
| + | * Compute the shared DH secret. | ||
| + | * Derive a symmetric key from the shared secret. | ||
| - | - Send the first SELECT command with `1PAY.SYS.DDF01': 00A404000E315041592E5359532E4444463031 | + | Generating a signature over the RSA and DH public keys is a way to authenticate the remote host. If the client successfully verifies this signature using the server's public key, then the server is authenticated unless the public key itself has been replaced by the attacker in a man in the middle attack. We will look at a way to (mostly) solve this issue in the next task. |
| - | - Decode the response using [[http://www.emvlab.org|emvlab]]. Use the SFI response (e.g. 01, concatenated with the record number encoded in the last 3 bits): (SFI << 3) | REC_NUM. E.g. If SFI=01 and REC_NUM=1, we get the Reference Control parameter (P2) 0x0C for the READ RECORD command, leading to the READ RECORD command 00B2010C00. | + | |
| - | - Check the available apps by sending READ RECORD commands of the form 00B2010C00, 00B2020C00, etc. Check the responses by decoding them with [[http://www.emvlab.org|emvlab]] | + | |
| - | - Eventually select one of them using SELECT, e.g. | + | |
| - | * select particular app: 00A4040007XXXXXXXXXXXXXX (replace the X values based on the Application ID response to the 00B2XXX command above). | + | |
| - | E.g. to get something like 00A4040007A0000000041010. (If the application has 7 bytes -- 14 hex characters for the Application ID). | + | |
| - | * 00A4040007A0000000041010 (this must be updated for your card, based on the response to the 00B2XXX command above). | + | |
| - | * start transaction with GET PROCESSING OPTS: 80A80000028300 | + | |
| - | Now your terminal.txt file should look something like this | + | The symmetric key derived from the shared secret will be used to encrypt the communication between the client and server. Although we're stopping the tasks here, this key would be the one that you would use to encrypt and decrypt the communication between the client and server. If you want, you can check the [[https://pycryptodome.readthedocs.io/en/latest/src/cipher/aes.html |PyCryptodome documentation]] for more information on how to use AES. To see what ciphers SSH uses, run the following command: |
| - | (but again, replace the Application ID with the correct one and also use the correct READ RECORD commands -- from your trials). | + | |
| - | <code - terminal.txt> | + | |
| - | 00A404000E315041592E5359532E4444463031 | + | |
| - | 00B2010C00 | + | |
| - | 00A4040007A0000000041010 | + | |
| - | 80A80000028300 | + | |
| - | 0000000000 | + | |
| - | </code> | + | |
| - | As mentioned above, now run this terminal emulator with the following code: | + | <code bash> |
| - | <code> | + | ssh -Q cipher |
| - | python3 sclink.py --scterminal terminal.txt gg | + | |
| </code> | </code> | ||
| - | ==== Reading data from card ==== | + | ==== 2. Do you like TOFU? (3p) ==== |
| - | Your next goal is to be able to read all the application files with READ RECORD commands (for each file). | + | Now start solving the TODO 2 series by implementing Trust On First Use in ''%%dhe_client.py%%''. Do it as follows: |
| - | In order to find out the present files (which differ from card to card), you need to issue the GET PROCESSING OPTS command above (80A80000028300). | + | * Store the public key of the server in a file named ''%%known_hosts%%'' in the following format (the same way SSH does it): |
| - | In response you should get the Application Interchange Profile (AIP) bytes (2 bytes, coded according to Book 3, Appendix C) | + | <code text> |
| - | followed by a list of Application File Locators (AFL, coded as explained in Book 3, Section 10.2. | + | hostname1 public_key1 |
| + | hostname2 public_key2 | ||
| + | ... | ||
| + | </code> | ||
| - | After you decode this ([[http://www.emvlab.org/tlvutils/|TLV decodeer]] might help), you will find one or more groups of 4 bytes as follows: | + | * If the client already has a public key for the given IP of the server, check that the public key of the server matches the one that is stored (if it's a first connection - i.e., **First Use** - just store the public key and **Trust** the host). |
| - | * 1st byte: SFI << 3 | + | * If it matches, print a "connection established" message and proceed to use that key for verification of the signature over the DH share of the server. |
| - | * 2nd byte: first record_number | + | * If it doesn't match, print a suggestive error message and exit. |
| - | * 3rd byte: last record_number | + | |
| - | * 4th byte: [you don't need it] | + | |
| - | <note> | + | This is very similar to what SSH does when connecting to a server using a pair of public/private keys and is known as Trust On First Use (TOFU) authentication. |
| - | The response of the Get Processing Opts command can vary. Either it is a BER-TLV encoded value and you will see easily the AIP and AFL values, or it is a non TLV result (starting with tag 88) where the AIP (2 bytes) and the list of AFLs (each of 4 bytes) are just concatenated, i.e. you have something like 88 <LEN> <AIP> <AFL1> <AFL2> ... <AFLn>, where n>=1. Each AFL is encoded as mentioned above. | + | |
| - | </note> | + | |
| - | SFI is like a directory with multiple records that can be read. | + | ==== 3. PQC ready? (4p) ==== |
| - | To read a file, you need to issue a READ RECORD command which looks like this: | + | Now let's update the key exchange to use post-quantum cryptography. |
| - | 00B2 <record_number> <SFI || 100> | + | |
| - | The <record_number> is a byte (you need to write a READ RECORD command for each record_number). | + | For this you can either modify the work you did above to use a post-quantum key exchange method such as ML-KEM in Python. |
| - | <SFI || 100> is a byte which contains the SFI number in the first 5 bits and 100 in the last 3 bits. This is the same as SFI << 3 + 0x04. | + | To do this in Python you can use a library such as [[https://pypi.org/project/mlkem/|ml-kem]]. |
| - | + | You can install this as follows: | |
| - | For example, if your AFL shows like "10 01 05 01", then you might want to read records between 01 and 05 using SFI 01 + 0x04, i.e. issuing READ RECORD commands like this: | + | |
| <code> | <code> | ||
| - | 00 B2 01 14 00 | + | pip install ml-kem |
| </code> | </code> | ||
| - | ==== Read public key material ==== | + | Below is an example of how to use this library in a simple client-server scenario. Note: this example is provided by Gemini AI, so use it with caution and double-check it: |
| + | <code> | ||
| + | # Install the library first: pip install ml-kem | ||
| - | Using the READ RECORD commands mentioned earlier and the [[https://emvlab.org/tlvutils/|TLV decoder]], find the public keys in | + | from mlkem.ml_kem import ML_KEM |
| - | your card, in particular: | + | from mlkem.parameter_set import ML_KEM_768 # Recommended security level |
| + | import secrets | ||
| - | * Issuer public key certificate | + | # --- Server (Alice) Side --- |
| - | * Issuer public key exponent | + | |
| - | * Issuer public key reminder | + | |
| - | * ICC public key certificate | + | |
| - | * ICC public key exponent | + | |
| - | * ICC public key reminder | + | |
| - | <note> | + | def server_keygen(): |
| - | Depending on the application selected, you might have (or NOT) public keys available. If you don't find ones, then just select a different app at the beginning. | + | """Alice generates her ML-KEM key pair.""" |
| - | </note> | + | # Initialize ML-KEM with the desired security level |
| + | ml_kem = ML_KEM(parameters=ML_KEM_768, randomness=secrets.token_bytes) | ||
| + | |||
| + | # Generate the encapsulation key (ek, public) and decapsulation key (dk, private) | ||
| + | ek, dk = ml_kem.key_gen() | ||
| + | |||
| + | print("Alice: Generated Public Key (ek) and Private Key (dk).") | ||
| + | return ek, dk, ml_kem | ||
| - | ==== Get Dynamic signature from card ==== | + | def server_decapsulate(dk, c, ml_kem): |
| + | """Alice decapsulates the ciphertext to get the shared secret.""" | ||
| + | try: | ||
| + | K_prime = ml_kem.decaps(dk, c) | ||
| + | print("Alice: Successfully decapsulated the Shared Secret (K').") | ||
| + | return K_prime | ||
| + | except ValueError as e: | ||
| + | print(f"Alice: Decapsulation failed! {e}") | ||
| + | return None | ||
| - | After you get all the public key data, use an INTERNAL AUTHENTICATE command similar to this: 00880000043085C163. | + | # --- Client (Bob) Side --- |
| - | See the file trace_emv.txt for an example of trace as model for the set of commands you might have to issue (i.e. to add to your terminal.txt file) [[https://ocw.cs.pub.ro/courses/_media/ac/laboratoare/lab_emv.zip|here]]. | + | |
| + | def client_encapsulate(ek): | ||
| + | """Bob encapsulates a shared secret using Alice's public key.""" | ||
| + | ml_kem = ML_KEM(parameters=ML_KEM_768, randomness=secrets.token_bytes) | ||
| + | | ||
| + | # Encapsulate to get the shared secret (K) and the ciphertext (c) | ||
| + | K, c = ml_kem.encaps(ek) | ||
| + | | ||
| + | print("Bob: Encapsulated a Shared Secret (K) and created Ciphertext (c).") | ||
| + | return K, c | ||
| - | As discussed in class (see also the [[https://www.emvco.com/specifications/book-2-security-and-key-management-2/|EMV book 2]], section 6), modern EMV cards generally support dynamic signature generation (DDA). | + | # --- Communication Flow Simulation --- |
| - | This works as follows: | + | |
| - | * The terminal issues the INTERNAL AUTHENTICATE command with some random data (typically 4 bytes) | + | |
| - | * The ICC makes a signature over some internal ICC data and the random bytes from the terminal | + | |
| - | * The ICC sends the signature (signed dynamic data) to the terminal in response to the INTERNAL AUTHENTICATE command | + | |
| - | * The terminal verifies the signature using a chain of certificates | + | |
| - | An example of an INTERNAL AUTHENTICATE command similar is the following: 00880000043085C163. | + | # 1. Server (Alice) Key Generation |
| - | You can look at the file trace_emv.txt for an example of trace [[https://ocw.cs.pub.ro/courses/_media/ac/laboratoare/lab_emv.zip|here]] | + | ek_server, dk_server, ml_kem_instance = server_keygen() |
| - | <note> | + | # 2. Public Key Transmission (ek_server is sent to the client) |
| + | print("\n--- Network Transmission: ek sent to Bob ---") | ||
| - | If your card doesn't work with the standard Payment application ID (the one in terminal.txt), try using one from | + | # 3. Client (Bob) Encapsulation |
| - | [[https://www.eftlab.com/knowledge-base/211-emv-aid-rid-pix/|here]]. | + | K_client, c_client = client_encapsulate(ek_server) |
| - | A short list might be this one: | + | # 4. Ciphertext Transmission (c_client is sent back to the server) |
| - | <code> | + | print("\n--- Network Transmission: c sent to Alice ---") |
| - | // EMV.AIDLIST: | + | |
| - | EMV.AIDLIST = new Array(); | + | |
| - | EMV.AIDLIST[0] = { aid : "A00000002501", partial : true, name : "AMEX" }; | + | |
| - | EMV.AIDLIST[1] = { aid : "A0000000031010", partial : false, name : "VISA" }; | + | |
| - | EMV.AIDLIST[2] = { aid : "A0000000041010", partial : false, name : "MC" }; | + | |
| - | </code> | + | |
| - | Check that you obtained a correct DDA signature and a successful "9000" response. | + | # 5. Server (Alice) Decapsulation |
| + | K_server = server_decapsulate(dk_server, c_client, ml_kem_instance) | ||
| - | To verify the DDA signature obtained earlier, the terminal must have access to the root CA public keys. | + | # 6. Verification |
| - | You may find some of these available | + | print("\n--- Verification ---") |
| - | [[https://developer.elavon.com/na/docs/viaconex/1.0.0/emv-integration-guide/10_references/emv_production_public_keys.md|here]], | + | if K_client is not None and K_server is not None: |
| - | [[https://technologypartner.visa.com/download.aspx?id=34|here]], | + | if K_client == K_server: |
| - | [[https://www.mastercard.us/content/dam/public/mastercardcom/na/us/en/documents/mchip-payment-system-public-keys-12042018.pdf|here]], | + | print("Success! Alice's and Bob's shared secrets match.") |
| - | or | + | # The shared secret can now be used as an AES key, e.g., K_client |
| - | [[https://www.eftlab.com/knowledge-base/243-ca-public-keys/|here]]. | + | # The shared secret is bytes: |
| + | # print(f"Shared Secret: {K_client.hex()}") | ||
| + | else: | ||
| + | print("Failure! Shared secrets do not match.") | ||
| + | else: | ||
| + | print("Failure in key exchange process.") | ||
| + | </code> | ||
| - | You will need to know the card type (AMEX, VISA, Mastercard, etc.) and CA public key index, which is given by the ICC (see tag 8F). | ||
| + | Otherwise, you can start from the Diffie-Hellman key exchange lab we did in OpenSSL/C. You may start from {{:ac:laboratoare:lab_dhe_solved.zip|this}} code, that provides a working solution for the Diffie-Hellman lab in OpenSSL. | ||
| - | </note> | + | If you go for the C implementation in OpenSSL, one option to include post-quantum encryption is using [[https://github.com/open-quantum-safe/liboqs |liboqs]]. To install it on Ubuntu, use the command below: |
| - | The process to verify a DDA signature is as follows: | + | <code> |
| - | * The terminal verifies (RSA decrypts) the signed Issuer public key data (read from the ICC) using the CA public key, obtaining the Issuer public key | + | sudo apt install astyle cmake gcc ninja-build libssl-dev python3-pytest python3-pytest-xdist unzip xsltproc doxygen graphviz python3-yaml valgrind |
| - | * The terminal verifies (RSA decrypts) the signed ICC public key data (read from the ICC) using the Issuer public key, obtaining the ICC public key | + | </code> |
| - | * The terminal verifies (RSA decrypts) the signed DDA data using the ICC public key (read from the ICC via the INTERNAL AUTHENTICATE command) | + | |
| - | At each step, the verification step includes decryption of the data and checking that the hash over the fields mentioned in Book 2 of EMV matches the hash in the decrypted data. | + | If using this library, you may use [[https://github.com/open-quantum-safe/liboqs/blob/main/tests/example_kem.c |this example]] to implement your own version of key exchange and test it in the scenarios above (key exchange and key exchange + TOFU). You may use any resources or available KEMs. |
| - | === Verify the signature returned by your card === | + | Alternatively, you can also use OpenSSL directly (starting with version 3.5). You can find some documentation for ML-KEM [[https://docs.openssl.org/master/man7/EVP_KEM-ML-KEM/ |here]] and [[https://docs.openssl.org/3.5/man7/EVP_PKEY-ML-KEM/ |here]]. |
| - | In short, you need to do the following (see EMV Book 2, sections 6, 6.1, 6.2, 6.3, 6.4 and 6.5) | + | Below is an example code obtained through Gemini AI (not fully tested, but perhaps a good starting point): |
| - | * Decrypt Issuer public key from Issuer Certificate Public key using the root CA public key of your card scheme (section 6.3) | + | <code> |
| - | * Decrypt ICC public key from ICC Certificate Public Key using the Issuer public key (section 6.4) | + | #include <stdio.h> |
| - | * Decrypt DDA signature returned by the card using the ICC public key (section 6.5) | + | #include <stdlib.h> |
| - | * Verify the DDA signature (section 6.5) | + | #include <string.h> |
| + | #include <openssl/evp.h> | ||
| + | #include <openssl/err.h> | ||
| + | #include <openssl/provider.h> | ||
| - | To recover the data from the issuer public key certificate (same applies to the other signatures), you may also find useful the following notes (which are based on the EMV specs mentioned above, please refer to those). | + | // --- HARDCODED ML-KEM-768 Public Key (Replace with a real key) --- |
| + | // A real ML-KEM-768 public key is 608 bytes long. | ||
| + | // This is a placeholder for demonstration. | ||
| + | unsigned char server_public_key_bytes[] = { | ||
| + | 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, | ||
| + | // ... 592 more bytes of a real ML-KEM-768 public key would go here ... | ||
| + | 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, | ||
| + | // Note: This placeholder is NOT a valid ML-KEM-768 key and will likely fail. | ||
| + | // Replace with a 608-byte key generated via 'openssl genpkey' for real use. | ||
| + | }; | ||
| + | size_t public_key_len = 608; // ML-KEM-768 Public Key size | ||
| - | * First, generate a template ASN1 file as follows: | + | // Error handling macro |
| - | <file asn1 'template.asn1'> | + | #define HANDLE_ERROR(msg) \ |
| - | # Start with a SEQUENCE | + | { \ |
| - | asn1=SEQUENCE:pubkeyinfo | + | fprintf(stderr, "%s failed.\n", msg); \ |
| + | ERR_print_errors_fp(stderr); \ | ||
| + | goto cleanup; \ | ||
| + | } | ||
| - | # pubkeyinfo contains an algorithm identifier and the public key wrapped | + | int main(void) { |
| - | # in a BIT STRING | + | EVP_PKEY_CTX *kctx = NULL; |
| - | [pubkeyinfo] | + | EVP_PKEY *peer_pub_key = NULL; |
| - | algorithm=SEQUENCE:rsa_alg | + | OSSL_PARAM params[2]; |
| - | pubkey=BITWRAP,SEQUENCE:rsapubkey | + | unsigned char *shared_secret = NULL; |
| + | unsigned char *ciphertext = NULL; | ||
| + | size_t shared_secret_len = 0; | ||
| + | size_t ciphertext_len = 0; | ||
| + | int ret = 0; | ||
| - | # algorithm ID for RSA is just an OID and a NULL | + | printf("Starting ML-KEM Client Key Encapsulation (ML-KEM-768).\n"); |
| - | [rsa_alg] | + | |
| - | algorithm=OID:rsaEncryption | + | |
| - | parameter=NULL | + | |
| - | # Actual public key: modulus and exponent | + | // 1. Load the OpenSSL default provider (ML-KEM is in the default provider since 3.5) |
| - | [rsapubkey] | + | if (!OSSL_PROVIDER_load(NULL, "default")) { |
| - | n=INTEGER:0x%%MODULUS%% | + | HANDLE_ERROR("Failed to load default provider"); |
| + | } | ||
| - | e=INTEGER:0x%%EXPONENT%% | + | // 2. Import the raw public key bytes into an EVP_PKEY structure |
| - | </file> | + | printf("2. Importing ML-KEM-768 Public Key...\n"); |
| - | * Then use this template for all the keys you need to generate. For example, for the CA root key, use the template and replace the %%MODULUS%% and %%EXPONENT%% part by the modulus and exponent bytes given in the list of public CA root public keys for your card. Say the resulting file is named ca_pk.asn1. | + | // a. Create parameter array to describe the key components |
| - | * Then use openssl asn1 parser to obtain a public key in DER format as follows: | + | params[0] = OSSL_PARAM_construct_octet_string("pub", server_public_key_bytes, public_key_len); |
| - | <code> | + | params[1] = OSSL_PARAM_construct_end(); |
| - | openssl asn1parse -genconf ca_pk.asn1 -out ca_pk.der -noout | + | |
| - | </code> | + | |
| - | * Now copy the Issuer Certificate Public Key bytes obtained from the card into a file, say issuer_pk.bytes and then convert this to a binary file like this: | + | |
| - | <code> | + | |
| - | cat issuer_pk.bytes | xxd -r -p > issuer_pk.bin | + | |
| - | </code> | + | |
| - | * At this point you can verify/decrypt the issuer certificate using openssl as follows: | + | |
| - | <code> | + | |
| - | openssl rsautl -verify -in issuer_pk_cert.bin -inkey ca_pk.der -pubin -keyform DER -raw | + | |
| - | </code> | + | |
| - | Although it might be more convenient to see the output in hexa, using something like this: | + | |
| - | <code> | + | |
| - | openssl rsautl -verify -in issuer_pk_cert.bin -inkey ca_pk.der -pubin -keyform DER -raw | xxd -p | + | |
| - | </code> | + | |
| - | + | ||
| - | To understand the meaning of the decrypted bytes, please refer to the respective EMV documentation (in particular sections 6.2-6.5 | + | |
| - | in book 2). For example, for the Issuer public key certificate, to obtain the actual issuer public key you need to ignore the first | + | |
| - | 15 bytes (metadata) as well as the last 21 bytes (hash result and trailer value "BC"). The reminder bytes are the first part of the | + | |
| - | Issuer Public key. For the second part of the Issuer Public key (which you need to concatenate to the first part to get the full | + | |
| - | public key), please see the card response with tag 92 (Issuer Public Key reminder). | + | |
| - | Apply the same/similar process to get the ICC public key and finally to decrypt/verify the DDA signature. | + | // b. Import the key. ML-KEM-768 is named "ML-KEM-768" |
| + | peer_pub_key = EVP_PKEY_new_from_params(NULL, params, "ML-KEM-768"); | ||
| + | if (!peer_pub_key) { | ||
| + | // NOTE: This will fail if the hardcoded bytes are not a valid key. | ||
| + | HANDLE_ERROR("EVP_PKEY_new_from_params failed (Public Key Import)"); | ||
| + | } | ||
| - | <note> | + | // 3. Initialize the Key Encapsulation Context |
| - | Check the decrypted DDA response format in the EMV specs (book 2, section 6.5). | + | kctx = EVP_PKEY_CTX_new_from_pkey(NULL, peer_pub_key, NULL); |
| - | The response should follow this format (but please check it to confirm): | + | if (!kctx) { |
| - | * response length (1 byte) -- say N | + | HANDLE_ERROR("EVP_PKEY_CTX_new_from_pkey failed"); |
| - | * signature (N - 21 bytes) | + | } |
| - | * a SHA-1 hash over 20 bytes (20*8 = 160 bits) | + | |
| - | * a trailer byte with value "BC" | + | |
| - | The hash contained in the DDA reponse is computed over the signature bytes (N - 21 bytes) concatenated with the data sent for the DDA signature (typically the 4 random/unpredictable bytes). Hence, if you recompute the hash over the N-21 signature bytes concatenated with the 4 random bytes and this matches the 20 bytes of the hash in the DDA response this should confirm that the 4 random bytes were correctly input into the signature generation. | + | if (EVP_PKEY_encapsulate_init(kctx) <= 0) { |
| - | </note> | + | HANDLE_ERROR("EVP_PKEY_encapsulate_init failed"); |
| + | } | ||
| + | // 4. Determine buffer sizes | ||
| + | printf("4. Determining buffer sizes...\n"); | ||
| + | if (EVP_PKEY_encapsulate(kctx, NULL, &shared_secret_len, NULL, &ciphertext_len) <= 0) { | ||
| + | HANDLE_ERROR("EVP_PKEY_encapsulate (size determination) failed"); | ||
| + | } | ||
| + | | ||
| + | printf(" - Shared Secret Length: %zu bytes\n", shared_secret_len); | ||
| + | printf(" - Ciphertext Length: %zu bytes\n", ciphertext_len); | ||
| - | === Responses from a card === | + | // 5. Allocate buffers |
| + | shared_secret = OPENSSL_malloc(shared_secret_len); | ||
| + | ciphertext = OPENSSL_malloc(ciphertext_len); | ||
| + | if (!shared_secret || !ciphertext) { | ||
| + | HANDLE_ERROR("Memory allocation failed"); | ||
| + | } | ||
| - | === In case of trouble use existing signature === | + | // 6. Perform the Encapsulation (Client's Key Exchange Step) |
| + | printf("6. Performing Key Encapsulation...\n"); | ||
| + | if (EVP_PKEY_encapsulate(kctx, shared_secret, &shared_secret_len, ciphertext, &ciphertext_len) <= 0) { | ||
| + | HANDLE_ERROR("EVP_PKEY_encapsulate failed"); | ||
| + | } | ||
| - | If you don't manage to get a signature from your card, use these responses from a card (decode them with TLV decode): | + | printf("✅ Encapsulation successful!\n"); |
| + | printf(" The client has generated a **Shared Secret** and the **Ciphertext**.\n"); | ||
| + | |||
| + | printf(" - Shared Secret (First 16 bytes): "); | ||
| + | for (size_t i = 0; i < (shared_secret_len > 16 ? 16 : shared_secret_len); i++) { | ||
| + | printf("%02x", shared_secret[i]); | ||
| + | } | ||
| + | printf("...\n"); | ||
| + | printf(" - Ciphertext (First 16 bytes, to be sent to Server): "); | ||
| + | for (size_t i = 0; i < (ciphertext_len > 16 ? 16 : ciphertext_len); i++) { | ||
| + | printf("%02x", ciphertext[i]); | ||
| + | } | ||
| + | printf("...\n"); | ||
| - | Start from the following responses of a card (decode them with [[http://www.emvlab.org/tlvutils/|TLV decode]]) | + | ret = 0; // Success |
| + | |||
| + | cleanup: | ||
| + | // Free all allocated resources | ||
| + | EVP_PKEY_CTX_free(kctx); | ||
| + | EVP_PKEY_free(peer_pub_key); | ||
| + | OPENSSL_free(shared_secret); | ||
| + | OPENSSL_free(ciphertext); | ||
| + | OSSL_PROVIDER_unload(OSSL_PROVIDER_load(NULL, "default")); // Unload provider | ||
| - | <code> | + | if (ret != 0) { |
| - | > 00 B2 01 14 8A | + | return EXIT_FAILURE; |
| - | < 70 81 87 5F 25 03 08 02 01 5F 24 03 12 02 29 5A 08 54 00 49 51 48 65 15 96 5F 34 01 00 9F 07 02 FF 00 8E 14 00 00 00 00 00 00 00 00 42 01 44 03 41 03 42 03 5E 03 1F 03 8C 21 9F 02 06 9F 03 06 9F 1A 02 95 05 5F 2A 02 9A 03 9C 01 9F 37 04 9F 35 01 9F 45 02 9F 4C 08 9F 34 03 8D 0C 91 0A 8A 02 95 05 9F 37 04 9F 4C 08 9F 0D 05 F8 50 AC 08 00 9F 0E 05 00 00 00 00 00 9F 0F 05 F8 70 AC 98 00 5F 28 02 06 42 9F 4A 01 82 | + | } |
| + | return EXIT_SUCCESS; | ||
| + | } | ||
| </code> | </code> | ||
| + | |||
| + | Some instructions to use this code (also from Gemini AI): | ||
| + | * Save the code as mlkem_client.c. | ||
| + | * Generate a real ML-KEM-768 public key (if you didn't do this in the previous step) by running the following commands: | ||
| <code> | <code> | ||
| - | > 00 B2 02 14 47 | + | openssl genpkey -algorithm ML-KEM-768 -out server_private.pem |
| - | < 70 45 9F 08 02 00 02 57 13 54 00 49 51 48 65 15 96 D1 20 22 01 00 00 08 28 00 00 0F 5F 20 17 43 48 4F 55 44 41 52 59 20 2F 4F 4D 41 52 20 53 41 4C 49 4D 20 44 4C 5F 30 02 02 01 9F 42 02 09 78 9F 44 01 02 8F 01 04 | + | openssl pkey -in server_private.pem -pubout -out server_public.pem |
| </code> | </code> | ||
| + | * Extract the raw public key bytes to replace the placeholder in the C code. A common way to get the raw bytes is to convert the PEM file to a DER format and extract the key component. For ML-KEM, the raw key is often found within the ASN.1 structure. | ||
| + | <note> | ||
| + | See the DH lab for information about this process. But see also below more details for this process, as provided by Gemini AI. | ||
| + | </note> | ||
| + | * Compile the code (adjusting paths to your OpenSSL 3.5 installation), using a command like this (check again also the DH lab for more details): | ||
| <code> | <code> | ||
| - | > 00 B2 03 14 96 | + | gcc -o mlkem_client mlkem_client.c -I/path/to/openssl/include -L/path/to/openssl/lib -lssl -lcrypto |
| - | < 70 81 93 90 81 90 0B 69 37 0D CF E1 E7 B0 9C 00 6F CC 12 91 38 C0 7A 69 80 87 3C 1E 0A 60 04 E6 8E 23 F5 BF B7 51 08 28 00 8B 37 F4 C3 D3 30 6A 0D AE 70 92 51 2F FB B1 E8 1E AE 26 23 1A 0D BF C8 30 B3 1C F1 F6 81 9C F3 12 37 FE 74 B3 5C 5B 57 62 0A 4D C1 96 ED 06 CC 94 45 AC 0A 5B 00 BB 8E BA 7F B4 1D 97 4C A1 F9 DD A4 45 1E B3 2E FC 55 5A 16 9D 60 09 47 4E 97 09 2B 33 21 AD D5 9D 1C 35 30 11 CC C1 C1 D6 19 65 B9 12 0E 07 FC 8F B3 72 4A C0 3A 15 | + | |
| </code> | </code> | ||
| + | |||
| + | The following GeminiAI-generated scripts (might have some bugs, please double-check, they are only given as reference/helper tools) generate an ML-KEM-768 key pair and, importantly, forces OpenSSL to save the private key in the FIPS 203 'dk' format (the one the C code expects for decapsulation) rather than the default 'seed' format, which is not what the raw C code imports: | ||
| <code> | <code> | ||
| - | > 00 B2 04 14 CA | + | #!/bin/bash |
| - | < 70 81 C7 9F 32 01 03 92 24 EF 2F D9 E8 3B D9 0C 88 A1 A6 58 84 B3 5A 63 69 08 40 36 59 83 1A FB 41 74 3C 61 E5 0F CE 32 49 1C D3 9A 81 93 81 90 86 09 6D 87 91 88 BB 1C 0C 5C C2 3D E0 85 79 96 E8 50 46 9B FF 6A F9 39 C7 3C D6 DE 78 67 75 F3 F7 1E C1 94 B0 05 2E B1 CE C5 2B 02 BD 41 CD 2D BA A5 E0 84 C5 29 00 68 90 17 FD 57 A8 4B 47 BC 8D 33 CD 1E D5 B4 81 87 B1 E0 82 F2 5B 4E F1 32 89 65 5A 2E 66 90 1F 98 96 9D 9E 9C C3 A7 30 D0 84 D5 FA DD 83 EA A7 9B 28 2B 02 E4 AA CB 5C E8 FC AC 77 0A FD EA EB 4B 8E 37 51 AD D8 25 F3 7B 36 DB 5E 45 BE D7 23 3A 50 85 29 8E 98 0B 07 5C 9F 49 03 9F 37 04 9F 47 01 03 | + | KEY_ALG="ML-KEM-768" |
| + | PRIV_FILE="server_private_dk.pem" | ||
| + | PUB_FILE="server_public_ek.pem" | ||
| + | RAW_PRIV_FILE="server_private_raw.bin" | ||
| + | RAW_PUB_FILE="server_public_raw.bin" | ||
| + | |||
| + | echo "--- 1. Generating $KEY_ALG Key Pair (forcing dk-only private key) ---" | ||
| + | |||
| + | # Generate the private key, using '-provparam ml-kem.retain_seed=no' | ||
| + | # to ensure it saves the full FIPS 203 'dk' decapsulation key, | ||
| + | # NOT just the seed. | ||
| + | openssl genpkey -algorithm "$KEY_ALG" \ | ||
| + | -provparam ml-kem.retain_seed=no \ | ||
| + | -out "$PRIV_FILE" | ||
| + | |||
| + | # Extract the public key from the private key file | ||
| + | openssl pkey -in "$PRIV_FILE" -pubout -out "$PUB_FILE" | ||
| + | |||
| + | echo "Keys generated: $PRIV_FILE and $PUB_FILE" | ||
| + | echo "----------------------------------------------------------------" | ||
| </code> | </code> | ||
| + | |||
| + | The ML-KEM public key is in the SubjectPublicKeyInfo structure in the PEM file. We use openssl pkey to output the key in DER format and then remove the ASN.1 header/wrapper to get the raw bytes: | ||
| <code> | <code> | ||
| - | > 00 B2 05 14 B4 | + | echo "--- 2. Extracting RAW Public Key (608 bytes) ---" |
| - | < 70 81 B1 9F 46 81 90 8A A7 53 01 F7 40 51 03 68 95 94 6E EC CE 31 CF 9E CA A4 5E 77 FF 4F A3 39 B2 BF F9 00 49 15 26 A0 82 1F 5B 42 F5 7A 78 BD F8 4A 5B 6F C6 37 56 1B 1B F5 FB 68 C9 2E 58 C4 31 F1 D6 85 DE 5C F7 C1 9D BF 3A 05 E9 46 DC 6A C7 E6 E7 4B 43 A8 6F C0 FE 1C E9 6F C2 EB 86 F2 05 0B C1 AC 0F 70 1D B0 A9 38 84 2D BD AA F0 2D B7 9B C9 32 D2 0E 62 62 8C C4 01 F4 FA 19 93 36 0B 7C 30 FD A6 6C CF 35 C3 8F FF A1 71 19 4C 58 53 E1 E9 F0 08 59 8B 9F 48 1A F0 62 87 E0 BA F1 0B 92 F5 8E 4A 7A 09 8C 49 EF 3E 38 31 58 F6 FF B3 45 39 85 | + | |
| + | # The ML-KEM public key is 608 bytes long. | ||
| + | # The 'pubout' option converts to DER, and 'tail' removes the ASN.1 wrapper. | ||
| + | # NOTE: The ASN.1 wrapper size may vary by OpenSSL version/config. We must | ||
| + | # determine the exact offset to trim off the SubjectPublicKeyInfo header. | ||
| + | # For standard ML-KEM-768 PKCS#8 output, the header is typically 24 bytes. | ||
| + | HEADER_BYTES=24 # Common offset for ML-KEM-768 PKCS#8 SubjectPublicKeyInfo header | ||
| + | |||
| + | openssl pkey -in "$PUB_FILE" -pubin -outform DER -out "$RAW_PUB_FILE" | ||
| + | # Trim the ASN.1 header to get the raw 608-byte key | ||
| + | # (Skip first $HEADER_BYTES, save the next 608 bytes) | ||
| + | dd if="$RAW_PUB_FILE" of="client_public_key_raw_final.bin" bs=1 skip=$HEADER_BYTES count=608 status=none | ||
| + | |||
| + | echo "Raw public key saved to client_public_key_raw_final.bin" | ||
| + | # Print the hex array for C code | ||
| + | echo "Public Key Hex Array:" | ||
| + | xxd -p "client_public_key_raw_final.bin" | tr -d '\n' | sed 's/../&, 0x/g' | sed 's/^, 0x/0x/' | sed 's/, $//' | ||
| + | echo | ||
| + | echo "----------------------------------------------------------------" | ||
| </code> | </code> | ||
| + | |||
| + | Similarly, the private key (the FIPS 203 'dk' component) is inside the PKCS#8 private key structure. We again convert to DER and trim the ASN.1 wrappers: | ||
| <code> | <code> | ||
| - | > 00 88 00 00 04 30 85 C1 63 | + | echo "--- 3. Extracting RAW Private Key (1184 bytes) ---" |
| - | < [] 61 87 | + | |
| - | 135 | + | # The ML-KEM-768 private key (dk) is 1184 bytes long. |
| - | > 00 C0 00 00 87 | + | # We convert to DER and again skip the ASN.1 header bytes. |
| - | < 77 81 84 9F 4B 81 80 99 0B 9E 27 E7 51 B7 45 5F F8 64 82 3C 82 35 94 CD E0 26 AB 9A D9 1D 02 7F 6E B8 5E DE 27 23 B7 81 40 0D BD 85 FD 20 21 07 08 A3 6C B3 09 51 33 B2 D7 30 BF 12 8B 23 C9 4D 87 3F 28 56 63 1C 19 F5 21 BB BC E2 2F 45 B2 C9 0A 7E B2 F5 C7 02 03 3C B3 AB 1C 06 F5 5A CB 44 3A E0 93 84 42 33 FF 16 D0 CE BB 75 6C E1 39 09 A6 39 5A 89 48 D1 9A BF D8 5E 29 43 0F A0 CC 16 90 7A 5C 92 CA 74 3F | + | # For PKCS#8 'dk' private key, the header is typically 50 bytes. |
| + | HEADER_BYTES=50 # Common offset for ML-KEM-768 PKCS#8 PrivateKeyInfo header | ||
| + | |||
| + | openssl pkey -in "$PRIV_FILE" -outform DER -out "$RAW_PRIV_FILE" | ||
| + | # Trim the ASN.1 header to get the raw 1184-byte key | ||
| + | # (Skip first $HEADER_BYTES, save the next 1184 bytes) | ||
| + | dd if="$RAW_PRIV_FILE" of="server_private_key_raw_final.bin" bs=1 skip=$HEADER_BYTES count=1184 status=none | ||
| + | |||
| + | echo "Raw private key saved to server_private_key_raw_final.bin" | ||
| + | # Print the hex array for C code | ||
| + | echo "Private Key Hex Array:" | ||
| + | xxd -p "server_private_key_raw_final.bin" | tr -d '\n' | sed 's/../&, 0x/g' | sed 's/^, 0x/0x/' | sed 's/, $//' | ||
| + | echo | ||
| + | echo "----------------------------------------------------------------" | ||
| </code> | </code> | ||
| - | For this card the public key modulus is this (1152 bit): | + | You can now use the hex output from these scripts to reliably replace the placeholder arrays in your C client and server code. |
| + | See also [[https://www.youtube.com/watch?v=FzLzH5W-mi8|this]] video about Post-Quantum Cryptography support in OpenSSL. | ||
| + | |||
| + | |||
| + | Below is also an example code from Gemini AI as starting point for the server, to pair with the code above for the client. | ||
| + | Note that these examples use hardcoded values for keys and do not imply any communication. Therefore, make sure you use them for a proper key exchange that uses communication between parties to exchange the public keys and ciphertexts, in a similar manner to what you did for the DH lab. | ||
| <code> | <code> | ||
| - | A6DA428387A502D7DDFB7A74D3F412BE762627197B25435B7A81716A700157DDD06F7CC99D6CA28C2470527E2C03616B9C59217357C2674F583B3BA5C7DCF2838692D023E3562420B4615C439CA97C44DC9A249CFCE7B3BFB22F68228C3AF13329AA4A613CF8DD853502373D62E49AB256D2BC17120E54AEDCED6D96A4287ACC5C04677D4A5A320DB8BEE2F775E5FEC5 | + | #include <stdio.h> |
| - | </code> | + | #include <stdlib.h> |
| + | #include <string.h> | ||
| + | #include <openssl/evp.h> | ||
| + | #include <openssl/err.h> | ||
| + | #include <openssl/provider.h> | ||
| - | And the exponent is 0x03. | + | // --- HARDCODED ML-KEM-768 Private Key (Replace with a real key) --- |
| + | // A real ML-KEM-768 private key is 1184 bytes long. | ||
| + | unsigned char server_private_key_bytes[] = { | ||
| + | 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, | ||
| + | // ... 1168 more bytes of a real ML-KEM-768 private key would go here ... | ||
| + | 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, | ||
| + | }; | ||
| + | size_t private_key_len = 1184; // ML-KEM-768 Private Key size | ||
| - | In case of big big trouble, is the ASN1 file that you should obtain for the Issuer Public Key: | + | // --- HARDCODED Ciphertext (Generated by the Client - Replace with real data) --- |
| - | <code asn1 issuer_pk.asn1> | + | // A real ML-KEM-768 ciphertext is 1088 bytes long. |
| - | # Start with a SEQUENCE | + | unsigned char client_ciphertext_bytes[] = { |
| - | asn1=SEQUENCE:pubkeyinfo | + | 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, |
| + | // ... 1072 more bytes of a real ML-KEM-768 ciphertext would go here ... | ||
| + | 0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, | ||
| + | }; | ||
| + | size_t ciphertext_len = 1088; // ML-KEM-768 Ciphertext size | ||
| - | # pubkeyinfo contains an algorithm identifier and the public key wrapped | + | // --- Hardcoded Shared Secret Size (32 bytes for ML-KEM-768) --- |
| - | # in a BIT STRING | + | size_t shared_secret_len = 32; |
| - | [pubkeyinfo] | + | |
| - | algorithm=SEQUENCE:rsa_alg | + | |
| - | pubkey=BITWRAP,SEQUENCE:rsapubkey | + | |
| - | # algorithm ID for RSA is just an OID and a NULL | + | // Error handling macro |
| - | [rsa_alg] | + | #define HANDLE_ERROR(msg) \ |
| - | algorithm=OID:rsaEncryption | + | { \ |
| - | parameter=NULL | + | fprintf(stderr, "%s failed.\n", msg); \ |
| + | ERR_print_errors_fp(stderr); \ | ||
| + | goto cleanup; \ | ||
| + | } | ||
| - | # Actual public key: modulus and exponent | + | int main(void) { |
| - | [rsapubkey] | + | EVP_PKEY_CTX *kctx = NULL; |
| - | n=INTEGER:0xcb3a3f60cceccabb300a57d0c7c7fc974a34d8fa4728b9bb4719fec80a41b0cd04eb2c9fdfdd9139f87de2b3cbee69ecdf2889a37888beadc7a5ed5cc51da52940b000ef806ab277d0276386493da941f390f8a1354a3040dc84a7611b0a6e46874efd463a0f0607459ea58eEF2FD9E83BD90C88A1A65884B35A636908403659831AFB41743C61E50FCE32491CD39A81 | + | EVP_PKEY *server_priv_key = NULL; |
| + | OSSL_PARAM params[2]; | ||
| + | unsigned char *recovered_secret = NULL; | ||
| + | int ret = 0; | ||
| - | e=INTEGER:0x3 | + | printf("Starting ML-KEM Server Key Decapsulation (ML-KEM-768).\n"); |
| - | </code> | + | |
| + | // 1. Load the OpenSSL default provider | ||
| + | if (!OSSL_PROVIDER_load(NULL, "default")) { | ||
| + | HANDLE_ERROR("Failed to load default provider"); | ||
| + | } | ||
| + | |||
| + | // 2. Import the raw private key bytes into an EVP_PKEY structure | ||
| + | printf("2. Importing ML-KEM-768 Private Key...\n"); | ||
| + | |||
| + | // a. Create parameter array to describe the key components | ||
| + | // The ML-KEM private key is referred to as "priv" (or 'dk' in FIPS 203 terms) | ||
| + | params[0] = OSSL_PARAM_construct_octet_string("priv", server_private_key_bytes, private_key_len); | ||
| + | params[1] = OSSL_PARAM_construct_end(); | ||
| + | |||
| + | // b. Import the key. | ||
| + | server_priv_key = EVP_PKEY_new_from_params(NULL, params, "ML-KEM-768"); | ||
| + | if (!server_priv_key) { | ||
| + | HANDLE_ERROR("EVP_PKEY_new_from_params failed (Private Key Import)"); | ||
| + | } | ||
| + | |||
| + | // 3. Initialize the Key Decapsulation Context | ||
| + | kctx = EVP_PKEY_CTX_new_from_pkey(NULL, server_priv_key, NULL); | ||
| + | if (!kctx) { | ||
| + | HANDLE_ERROR("EVP_PKEY_CTX_new_from_pkey failed"); | ||
| + | } | ||
| + | |||
| + | if (EVP_PKEY_decapsulate_init(kctx) <= 0) { | ||
| + | HANDLE_ERROR("EVP_PKEY_decapsulate_init failed"); | ||
| + | } | ||
| + | |||
| + | // 4. Allocate buffer for the shared secret | ||
| + | recovered_secret = OPENSSL_malloc(shared_secret_len); | ||
| + | if (!recovered_secret) { | ||
| + | HANDLE_ERROR("Memory allocation failed"); | ||
| + | } | ||
| + | |||
| + | // 5. Perform the Decapsulation (Server's Key Exchange Step) | ||
| + | printf("5. Performing Key Decapsulation...\n"); | ||
| + | |||
| + | // The ciphertext is passed as the input buffer (client_ciphertext_bytes). | ||
| + | // The recovered shared secret is written to the output buffer (recovered_secret). | ||
| + | if (EVP_PKEY_decapsulate(kctx, recovered_secret, &shared_secret_len, client_ciphertext_bytes, ciphertext_len) <= 0) { | ||
| + | HANDLE_ERROR("EVP_PKEY_decapsulate failed"); | ||
| + | } | ||
| + | |||
| + | printf("✅ Decapsulation successful!\n"); | ||
| + | printf(" The server has recovered the **Shared Secret**.\n"); | ||
| + | | ||
| + | printf(" - Recovered Shared Secret (First 16 bytes): "); | ||
| + | for (size_t i = 0; i < (shared_secret_len > 16 ? 16 : shared_secret_len); i++) { | ||
| + | printf("%02x", recovered_secret[i]); | ||
| + | } | ||
| + | printf("...\n"); | ||
| + | |||
| + | printf("\n**NOTE: For a successful key exchange, the hash of this secret must match the hash of the client's secret.**\n"); | ||
| + | |||
| + | ret = 0; // Success | ||
| + | | ||
| + | cleanup: | ||
| + | // Free all allocated resources | ||
| + | EVP_PKEY_CTX_free(kctx); | ||
| + | EVP_PKEY_free(server_priv_key); | ||
| + | OPENSSL_free(recovered_secret); | ||
| + | OSSL_PROVIDER_unload(OSSL_PROVIDER_load(NULL, "default")); // Unload provider | ||
| + | |||
| + | if (ret != 0) { | ||
| + | return EXIT_FAILURE; | ||
| + | } | ||
| + | return EXIT_SUCCESS; | ||
| + | } | ||
| + | </code> | ||