Examples of the Cryptography Library and Syscalls usage
Preliminary
In this document, we introduce concepts of asymmetric cryptography (also known as public key cryptography) and symmetric cryptography (known as secret key cryptography). In public key cryptography, a pair of keys is used: a private key which is kept secret and a public key which can be made public. One application of public key cryptography is digital signature when one needs to authenticate the origin of a message. In secret key cryptography, a single secret key is shared between two parties and is used to encrypt and decrypt a message: it is used to ensure the confidentiality of a message.
Elliptic curve cryptography is an implementation of public key cryptography that brings together the systems based on a specific mathematical object: an elliptic curve. The purpose of this document is to describe how the functions from the cryptography API should be called, so we won’t deep dive into the technical details of elliptic curves.
In the first section of the document, we will give examples of digital signatures with the curves Secp256k1, Ed25519 and Ed448 but there are many other curves that are supported.
In secret key cryptography, we usually use block ciphers like AES (the Advanced Encryption Standard) or DES (the Data Encryption Standard which was the first standard but has been replaced by AES). They are called “block” ciphers because they work on blocks. The block size for AES is 128 bits and the block size for DES is 64 bits. So, in order to encrypt long message, several block encryptions must be done. This is done by chaining the encryption (or the decryption). One method to encrypt (or decrypt) a message in a chained way is called CBC (Cipher Block Chaining), but there are other methods that are supported.
In the second section of the document, we give an example of how to encrypt and decrypt with AES in the CBC mode.
1. Digital signature algorithms
1.1. Elliptic Curve Digital Signature Algorithm (ECDSA)
ECDSA is a standard digital signature scheme relying on elliptic curves. A signature scheme usually consists of three algorithms:
- a key generation algorithm generates a pair of private key and public key
- a signature algorithm signs the digest of a message with the private key
- a verification algorithm verifies a signature with the public key
ECDSA uses Weierstrass curves, i.e. curves expressed with a (simple) Weierstrass equation: y^2 = x^3 + a * x + b
. a
and b
are parameters of the curve but they don’t need to be detailed in this document (we only write this equation to highlight the difference between a Weierstrass curve and a twisted Edwards curve introduced later in this document).
One of the most common (Weierstrass) curve is Secp256k1.
1.1.a. Key generation
The private key is a generated random integer while the public key is a point on the chosen curve. A point is represented by its coordinates (x,y)
which are each represented by an integer.
There are two ways to get the pair of keys:
- Generate both randomly with
cx_ecfp_generate_pair_no_throw
. In this case, the function initializes the structurescx_ecfp_private_key_t
andcx_ecfp_public_key_t
which hold the private key and the public key respectively. - Initialize the private key with
cx_ecfp_init_private_key_no_throw
given a bytes array and generate the public key withcx_ecfp_generate_pair_no_throw
.cx_ecfp_init_private_key_no_throw
initializes a structurecx_ecfp_private_key_t
which holds the private key used to sign a message andcx_ecfp_generate_pair_no_throw
calculates the public key and initializes a structurecx_ecfp_public_key_t
which holds the public key used to verify a signature.
For example, a pair of private/public key to be used with the curve Secp256k1 is generated as follows:
Case 1:
cx_ecfp_private_key_t privateKey;
cx_ecfp_public_key_t publicKey;
cx_ecfp_generate_pair_no_throw(CX_CURVE_SECP256K1, &publicKey, &privateKey, 0); /* the '0' indicates that a random private key should be generated */
Case 2:
uint8_t rawKey[32] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10,
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20}; /* The key in a bytes array */
cx_ecfp_private_key_t privateKey;
cx_ecfp_public_key_t publicKey;
cx_ecfp_init_private_key_no_throw(CX_CURVE_SECP256K1, rawKey, sizeof(rawKey), &privateKey);
cx_ecfp_generate_pair_no_throw(CX_CURVE_SECP256K1, &publicKey, &privateKey, 1); /* the '1' indicates that a private key has already been generated and initialized */
If one needs only to verify a given signature, there is no need to generated a pair of keys. Thus, only the public key has to be initialized with cx_ecfp_init_public_key_no_throw
given a bytes array representing the point with the prefix 04
(This convention must be respected).
In the following example, rawPKey
is a bytes array filled with the prefix 04
followed by the 32-byte x-coordinate and the 32-byte y-coordinate of the point representing the public key.
uint8_t rawPKey[65] = {0x04, 0x84, 0xbf, 0x75, 0x62, 0x26, 0x2b, 0xbd, 0x69,
0x40, 0x08, 0x57, 0x48, 0xf3, 0xbe, 0x6a, 0xfa,
0x52, 0xae, 0x31, 0x71, 0x55, 0x18, 0x1e, 0xce,
0x31, 0xb6, 0x63, 0x51, 0xcc, 0xff, 0xa4, 0xb0,
0x8c, 0xc4, 0x3d, 0x63, 0xb2, 0x85, 0x9d, 0x46,
0x9f, 0xee, 0x15, 0xf3, 0x1c, 0x9e, 0xdb, 0x53,
0x24, 0x26, 0x6e, 0x6f, 0xd0, 0x40, 0x7e, 0x87,
0x38, 0x2d, 0x60, 0xfc, 0x45, 0x11, 0xac, 0xd8};
cx_ecfp_public_key_t publicKey;
cx_ecfp_init_public_key_no_throw(CX_CURVE_SECP256K1, rawPKey, sizeof(rawPKey), &publicKey);
1.1.b. Signature
The signature function takes as input a private key, previously initialized, the digest of a message and outputs an encoded signature (DER encoded). The signature algorithm may be:
-
deterministic, i.e. the same signature is calculated each time, given the same inputs. In this case the mode
CX_RND_RFC6979
must be specified. -
random, i.e. the signature values change each time, even if the inputs are identical. In this case the mode
CX_RND_TRNG
must be specified.
The message shall be hashed to get the correct input length, depending on the chosen curve. Following the previous example with the curve Secp256k1, a message msg
is first hashed with the hash function SHA-256
as follows:
uint8_t msg[] = "sample";
uint8_t digest[CX_SHA256_SIZE]; /* CX_SHA256_SIZE is 32 bytes */
cx_hash_sha256(msg, sizeof(msg), digest, CX_SHA256_SIZE);
Assuming that privateKey
has been initialized with the curve Secp256k1 and the digest
of the message has been computed as in the previous example, an example of a random signature with the curve Secp256k1 is as follows:
uint8_t signature[256];
size_t signatureLen = sizeof(signature);
cx_ecdsa_sign_no_throw(&privateKey, CX_RND_TRNG, CX_SHA256, digest, CX_SHA256_SIZE, signature, &signatureLen, NULL)
1.1.c. Verification
The verification function takes as input the public key, the digest of the message and the corresponding (encoded) signature and returns 1
if the signature is verified and 0
otherwise.
Assuming that publicKey
has been initialized with the curve Secp256k1 and the signature signature
has been calculated as in the previous example, an example of a signature verification with the curve Secp256k1 is as follows:
cx_ecdsa_verify_no_throw(&publicKey, digest, CX_SHA256_SIZE, signature, signatureLen);
1.2. Edwards-curve Digital Signature Algorithm (EdDSA)
EdDSA is a digital signature scheme relying on specific curves, called Edwards curves. Especially, in this section, we consider twisted Edwards curves. The equation of these curves is different from the Weierstrass equation. They are defined by: a * x^2 + y^2 = 1 + d * x^2 * y^2
, where a
and d
are parameters of the curve.
We supported two twisted Edwards curves: Ed25519 and Ed448. As before, the signature scheme consists of three algortihms: key generation, signature and verification.
1.2.a Key generation
As for ECDSA, a pair of private key and public keys is generated with cx_ecfp_generate_pair_no_throw
or the private key is first initialized with cx_ecfp_init_private_key_no_throw
and the corresponding public key is generated and initialized with cx_ecfp_generate_pair_no_throw
.
The following code describes the initialization of the private key and the generation of a public key for Ed25519:
uint8_t rawKey[32]; /* All zero key */
cx_ecfp_private_key_t privateKey;
cx_ecfp_public_key_t publicKey;
cx_ecfp_init_private_key_no_throw(CX_CURVE_Ed25519, rawKey, 32, &private_key);
cx_ecfp_generate_pair_no_throw(CX_CURVE_Ed25519, &publicKey, &privateKey, 1);
In the case of Ed448:
uint8_t rawKey[57]; /* All zero key */
cx_ecfp_512_private_key_t privateKey;
cx_ecfp_512_public_key_t publicKey;
cx_ecfp_init_private_key_no_throw(CX_CURVE_Ed448, rawKey, 57, (cx_ecfp_private_key_t *)&private_key);
cx_ecfp_generate_pair_no_throw(CX_CURVE_Ed448, (cx_ecfp_public_key_t *)&publicKey, &privateKey, 1);
The generation of a random pair for Ed25519 is as follows:
cx_ecfp_private_key_t privateKey;
cx_ecfp_public_key_t publicKey;
cx_ecfp_generate_pair_no_throw(CX_CURVE_Ed25519, &publicKey, &privateKey, 0);
In the case of Ed448:
cx_ecfp_512_private_key_t privateKey;
cx_ecfp_512_public_key_t publicKey;
cx_ecfp_generate_pair_no_throw(CX_CURVE_Ed448, (cx_ecfp_public_key_t *)&publicKey, &privateKey, 0);
Depending on the length of the keys, one should use the correct structures to hold the keys. For Ed25519, cx_ecfp_private_key_t
(cx_ecfp_public_key_t
for the public key) is used since the private key is 32-byte long (the public key is 64-byte long), while the 57-byte (456 bits) private key used with Ed448 must be stored within cx_ecfp_512_private_key_t
and the 114-byte public key must be stored within cx_ecfp_512_public_key_t
.
1.2.b. Signature
The signature is performed by the function cx_eddsa_sign_no_throw
. In contrary to ECDSA, the digest of the message is calculated inside the function so it mustn’t be calculated before. However, the identifier of the algorithm used to compute the digest (hash function) must be specified. Usually, SHA-512
is used for Ed25519 and SHAKE-128
for Ed448. Here are some examples:
Assuming that the keys have been properly generated and initialized as in the previous examples, the signature of a message with the curve Ed25519 is obtained as follows:
uint8_t msg[] = "sample";
uint8_t signature[64]; /* The length of the signature is 64 bytes */
cx_eddsa_sign_no_throw(&privateKey, CX_SHA512, msg, sizeof(msg), signature, 64);
Similarly, the signature of a message with the curve Ed448 is obtained as follows:
uint8_t msg[] = "sample";
uint8_t signature[114]; /* The length of the signature is 114 bytes */
cx_eddsa_sign_no_throw((cx_ecfp_private_key_t *)&privateKey, CX_SHAKE128, msg, sizeof(msg), signature, 114);
1.2.c. Verification
The signature verification is performed by the function cx_eddsa_verify_no_throw
. As for the signature, the identifier of the hash function is specified.
The verification of the signature is done as follows for Ed25519:
cx_eddsa_verify_no_throw(&publicKey, CX_SHA512, msg, sizeof(msg), signature, 64);
And for Ed448:
cx_eddsa_verify_no_throw((cx_ecfp_public_key_t *)&publicKey, CX_SHAKE128, msg, sizeof(msg), signature, 114);
1.3. Schnorr signatures
Schnorr signatures are variants of ECDSA. In this document, we give examples for the variant specified in BIP0340 for the Taproot upgrade of Bitcoin. The curve used is Secp256k1.
1.3.a. Key generation
The key generation is the same as for ECDSA.
1.3.b. Signature
The signature is performed by the function cx_ecschnorr_sign_no_throw
which takes as input the private key, the message (not any digest), and an auxiliary data which is usually some random data. The auxiliary data is given to the function through the sig
parameter and the value CX_RND_PROVIDED
is set to the mode
parameter to specify this. The parameter sig
contains the calculated signature if the function returns properly.
In the example below, the value CX_ECSCHNORR_BIP0340
is also set to the mode
parameter to specify that the BIP-0340 implementation is used.
Thus, assuming a properly generated and initialized private key privateKey
, a message msg
and an auxiliary data aux
, the signature is done as follows:
uint8_t msg[32]; /* All zero message */
uint8_t aux[32]; /* All zero auxiliary data */
uint8_t signature[64];
size_t signatureLen = sizeof(signature);
memcpy(signature, aux, sizeof(aux)); /* First copy the auxiliary data to the signature array */
cx_ecschnorr_sign_no_throw(privateKey, CX_ECSCHNORR_BIP0340 | CX_RND_PROVIDED, CX_SHA256, msg, sizeof(msg), signature, &signatureLen);
In this example, the hash function used is SHA-256.
2. Encryption/Decryption
An encryption is the process of transforming an intelligible message to an unintelligible one. This transformation must be reversible, i.e. the intelligible message can be recovered by the recipient. This transformation is the decryption. Both encryption and decryption are parametrized by a secret key.
2.1. Key initialization
The secret key needs to be initialized before one calls the encryption or decryption function. Different sizes of key can be used so it must be specified first. According to the algorithm used, the structure cx_aes_key_t
or cx_des_key_t
is initialized given a bytes array with the function cx_aes_init_key_no_throw
or cx_des_init_key_no_throw
.
For example, to perform an AES encryption with a 128-bit key, the initialization step is done as follows:
uint8_t rawKey[16]; /* All zero key */
cx_aes_key_t aesKey;
cx_aes_init_key_no_throw(rawKey, sizeof(rawKey), &aesKey);
If a 192-bit key is used:
uint8_t rawKey[24];
cx_aes_key_t aesKey;
cx_aes_init_key_no_throw(rawKey, sizeof(rawKey), &aesKey);
If a 256-bit key is used:
uint8_t rawKey[32];
cx_aes_key_t aesKey;
cx_aes_init_key_no_throw(rawKey, sizeof(rawKey), &aesKey);
2.2. Encryption
The encryption function takes as input a secret key, an initialization vector (which is needed to perform a chained encryption), and the plaintext and outputs the ciphertext. Since the block cipher works on blocks, if the length of the plaintext is not a multiple of the block size, the plaintext must be padded, i.e. additional data is added at the end of the message to fulfill this requirement. This is done by specifying a type of padding method:
-
CX_PAD_ISO9797M1
to complete the plaintext with only0
. -
CX_PAD_ISO9797M2
to complete the plaintext with one1
, then all0
.
If the padding is not required, CX_PAD_NONE
is used.
The encryption is performed by cx_aes_iv_no_throw
or cx_des_iv_no_throw
depending on the algorithm. This function can perform both encryption and decryption so the value CX_ENCRYPT
must be passed to the mode
parameter for encryption and CX_DECRYPT
for decryption.
The encryption of a 32-byte plaintext using AES in CBC mode and with a 128-bit key is done as follows:
uint8_t plaintext[32]; /* All zero plaintext */
uint8_t iv[16]; /* The length of the initialization vector is always 16 */
uint8_t ciphertext[32];
size_t ciphertextLen = sizeof(ciphertext);
cx_aes_iv_no_throw(&aes_key, CX_ENCRYPT | CX_PAD_NONE | CX_CHAIN_CBC, iv, CX_AES_BLOCK_SIZE, plaintext, sizeof(plaintext), ciphertext, &ciphertextLen); /* CX_AES_BLOCK_SIZE is 16 bytes */
The encryption of a 21-byte plaintext using the padding method CX_PAD_ISO9797M1
(with AES in CBC mode):
uint8_t plaintext[21];
uint8_t iv[16]; /* The length of the initialization vector is always 16 */
uint8_t ciphertext[32];
size_t ciphertextLen = sizeof(ciphertext);
cx_aes_iv_no_throw(&aes_key, CX_ENCRYPT | CX_PAD_ISO9797M1 | CX_CHAIN_CBC, iv, CX_AES_BLOCK_SIZE, plaintext, sizeof(plaintext), ciphertext, &ciphertextLen);
2.3. Decryption
The decryption function takes as input a secret key, an initialization vector, and the ciphertext and outputs the plaintext. If the length of the plaintext that has been encrypted is not a multiple of the block size, the padding method used must be specified when calling the decryption function.
The decryption is performed by cx_aes_iv_no_throw
or cx_des_iv_no_throw
depending on the algorithm and with the mode value CX_DECRYPT
.
Assuming that the plaintext has been encrypted as in the previous example, the decryption of the obtained ciphertext using AES-CBC is as follows:
uint8_t plaintext[21];
uint8_t iv[16]; /* The length of the initialization vector is always 16 */
size_t plaintextLen = sizeof(plaintext);
cx_aes_iv_no_throw(&aes_key, CX_DECRYPT | CX_PAD_ISO9797M1 | CX_CHAIN_CBC, iv, CX_AES_BLOCK_SIZE, ciphertext, ciphertextLen, plaintext, &plaintextLen);